From 8c51698829be7e819bb5299e834db7ef74d0900c Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 2 Nov 2026 22:24:21 +0300 Subject: [PATCH 1/2] fix(javascript): keep backtrace coordinates when source map lookup misses (#601) source-map-js returns nulls for an unmapped position, and writing them onto the frame dropped the catcher location. --- workers/javascript/src/index.ts | 10 ++++++ workers/javascript/tests/index.test.ts | 50 ++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/workers/javascript/src/index.ts b/workers/javascript/src/index.ts index 84fae857..9a9966bc 100644 --- a/workers/javascript/src/index.ts +++ b/workers/javascript/src/index.ts @@ -210,6 +210,16 @@ export default class JavascriptEventWorker extends EventWorker { bias: SourceMapConsumer.LEAST_UPPER_BOUND, }); + /** + * source-map-js returns nulls when the generated position is not in the map. + * Assigning them would wipe the catcher coordinates and set sourceCode to []. + * + * Fixes bug: https://github.com/codex-team/hawk.workers/issues/600 + */ + if (!originalLocation.source) { + return stackFrame; + } + /** * Source code lines */ diff --git a/workers/javascript/tests/index.test.ts b/workers/javascript/tests/index.test.ts index 531826e8..8849b8e8 100644 --- a/workers/javascript/tests/index.test.ts +++ b/workers/javascript/tests/index.test.ts @@ -281,6 +281,56 @@ describe('JavaScript event worker', () => { await worker.finish(); }); + it('should keep the original frame when the source map has no mapping', async () => { + /** + * Arrange + */ + const worker = new JavascriptEventWorker(); + + jest.spyOn(worker, 'addTask'); + jest.spyOn(JavascriptEventWorker.prototype as any, 'loadSourceMapFile') + .mockResolvedValue(sourceMapFileContent); + await worker.start(); + + const workerEvent = createEventMock({ withBacktrace: true }); + const originalFrame = { + file: 'file:///main.js', + line: 9999, + column: 1, + function: 'handler', + }; + + /** + * Copy the frame so a mutation inside the worker cannot hide a failed lookup + */ + workerEvent.payload.backtrace = [ { ...originalFrame } ]; + + const release = createReleaseMock({ + projectId: workerEvent.projectId, + release: workerEvent.payload.release, + }); + + await db.collection('releases').insertOne(release); + + /** + * Act + */ + await worker.handle(workerEvent); + + /** + * Assert + */ + expect(worker.addTask).toHaveBeenCalledWith( + WorkerNames.GROUPER, + expect.objectContaining({ + payload: expect.objectContaining({ + backtrace: [ originalFrame ], + }), + }) + ); + await worker.finish(); + }); + it('should use cache while processing source maps', async () => { /** * Arrange From 205019f8d83f32921b608d6d363f3b9c01c0cca6 Mon Sep 17 00:00:00 2001 From: Kuchizu <70284260+Kuchizu@users.noreply.github.com> Date: Mon, 2 Nov 2026 23:56:00 +0300 Subject: [PATCH 2/2] Escape Markdown in Telegram notification titles (#599) * Escape Markdown in Telegram notification titles * Pin current date in paymaster tests to avoid month overflow * Escape project name in Telegram notification footer --------- Co-authored-by: Peter --- workers/paymaster/tests/index.test.ts | 6 +++--- workers/telegram/src/templates/event.ts | 13 +++++++++++-- workers/telegram/src/templates/several-events.ts | 5 +++-- workers/telegram/tests/provider.test.ts | 7 ++++++- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 23a1bd69..127f3401 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -543,7 +543,7 @@ describe('PaymasterWorker', () => { /** * Arrange */ - const currentDate = new Date(); + const currentDate = new Date('2005-12-22'); const paidUntil = new Date(currentDate.getTime()); paidUntil.setMonth(paidUntil.getMonth() + 3); @@ -590,7 +590,7 @@ describe('PaymasterWorker', () => { /** * Arrange */ - const currentDate = new Date(); + const currentDate = new Date('2005-12-22'); const paidUntil = new Date(currentDate.getTime()); paidUntil.setDate(paidUntil.getDate() + 1); @@ -647,7 +647,7 @@ describe('PaymasterWorker', () => { /** * Arrange */ - const currentDate = new Date(); + const currentDate = new Date('2005-12-22'); const lastChargeDate = new Date(currentDate.getTime()); lastChargeDate.setMonth(lastChargeDate.getMonth() - 1); // Set last charge date to 1 month ago diff --git a/workers/telegram/src/templates/event.ts b/workers/telegram/src/templates/event.ts index 46da0cba..8213e5b8 100644 --- a/workers/telegram/src/templates/event.ts +++ b/workers/telegram/src/templates/event.ts @@ -1,5 +1,14 @@ import type { EventsTemplateVariables, TemplateEventData } from 'hawk-worker-sender/types/template-variables'; +/** + * Escape characters that break Telegram legacy Markdown parsing + * + * @param text - raw text + */ +export function escapeMarkdown(text: string): string { + return text.replace(/[_*`[]/g, '\\$&'); +} + /** * Return tpl with data substitutions * @@ -17,10 +26,10 @@ export default function render(tplData: EventsTemplateVariables): string { location = '\n' + '```\n' + location + '\n```'; // markdown code block } - return ''.concat(event.payload.title, + return ''.concat(escapeMarkdown(event.payload.title), '\n', location || '', '\n', - `[View details](${eventURL}) `, `| *${tplData.project.name}*`, ` | ${eventInfo.newCount} new (${eventInfo.event.totalCount} total)` + `[View details](${eventURL}) `, `| ${escapeMarkdown(tplData.project.name)}`, ` | ${eventInfo.newCount} new (${eventInfo.event.totalCount} total)` ); } diff --git a/workers/telegram/src/templates/several-events.ts b/workers/telegram/src/templates/several-events.ts index 06325564..bb138f15 100644 --- a/workers/telegram/src/templates/several-events.ts +++ b/workers/telegram/src/templates/several-events.ts @@ -1,4 +1,5 @@ import type { EventsTemplateVariables } from 'hawk-worker-sender/types/template-variables'; +import { escapeMarkdown } from './event'; /** * Return tpl with data substitutions @@ -10,10 +11,10 @@ export default function render(tplData: EventsTemplateVariables): string { let message = tplData.events.length + ' new events\n\n'; tplData.events.forEach(({ event, newCount }) => { - message += `(${newCount}) ${event.payload.title} \n`; + message += `(${newCount}) ${escapeMarkdown(event.payload.title)} \n`; }); - message += `\n[View all events](${projectUrl}) | *${tplData.project.name}*`; + message += `\n[View all events](${projectUrl}) | ${escapeMarkdown(tplData.project.name)}`; return message; } diff --git a/workers/telegram/tests/provider.test.ts b/workers/telegram/tests/provider.test.ts index 51ba45c4..842a1861 100644 --- a/workers/telegram/tests/provider.test.ts +++ b/workers/telegram/tests/provider.test.ts @@ -2,7 +2,7 @@ import { EventNotification, SeveralEventsNotification } from 'hawk-worker-sender import { DecodedGroupedEvent, ProjectDBScheme } from '@hawk.so/types'; import TelegramProvider from 'hawk-worker-telegram/src/provider'; import templates from '../src/templates'; -import EventTpl from '../src/templates/event'; +import EventTpl, { escapeMarkdown } from '../src/templates/event'; import { ObjectId } from 'mongodb'; /** @@ -13,6 +13,11 @@ describe('TelegramProvider', () => { * Check templates rendering */ describe('templates', () => { + it('should escape Markdown entities in event titles', () => { + expect(escapeMarkdown('No such file: /home/market_static/*.csv [`x`]')) + .toBe('No such file: /home/market\\_static/\\*.csv \\[\\`x\\`]'); + }); + /** * Check that rendering of a single event message works without errors */