From ed1d3757075b7f4a4fc0eb0881bd3f4b92e62621 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:17:28 +0000 Subject: [PATCH 1/4] Initial plan From f3d08d01a479aceb0efe62806a5bbcca6c677996 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:21:23 +0000 Subject: [PATCH 2/4] Implement dot notation for nested objects in backtrace frame arguments Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- lib/memoize/index.test.ts | 114 ++++++++++--- lib/utils/payday.test.ts | 5 + ...0000-add-timestamp-index-to-repetitions.js | 141 ++++++++-------- ...7000000-create-release-project-id-index.js | 61 ++++++- ...104000000-add-timestamp-index-to-events.js | 142 ++++++++-------- ...0000-add-compound-index-to-daily-events.js | 146 ++++++++--------- workers/sender/src/index.ts | 3 + workers/sentry/src/index.ts | 7 + workers/sentry/src/utils/converter.ts | 47 +++++- workers/sentry/tests/converter.test.ts | 155 ++++++++++++++++++ workers/sentry/tests/index.test.ts | 2 +- 11 files changed, 576 insertions(+), 247 deletions(-) diff --git a/lib/memoize/index.test.ts b/lib/memoize/index.test.ts index 8953e00b..705bef95 100644 --- a/lib/memoize/index.test.ts +++ b/lib/memoize/index.test.ts @@ -20,9 +20,16 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with concat strategy across several calls', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param a + * @param b + */ @memoize({ strategy: 'concat', ttl: 60_000, max: 50 }) public async run(a: number, b: string) { this.calls += 1; @@ -47,9 +54,16 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with set of arguments with concat strategy across several calls', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param a + * @param b + */ @memoize({ strategy: 'concat' }) public async run(a: unknown, b: unknown) { this.calls += 1; @@ -84,9 +98,16 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value for stringified objects across several calls', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param x + * @param y + */ @memoize({ strategy: 'concat' }) public async run(x: unknown, y: unknown) { this.calls += 1; @@ -105,9 +126,15 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value for method with non-default arguments (NaN, Infinity, -0, Symbol, Date, RegExp) still cache same-args', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param {...any} args + */ @memoize({ strategy: 'concat' }) public async run(...args: unknown[]) { this.calls += 1; @@ -131,9 +158,15 @@ describe('memoize decorator — per-test inline classes', () => { it('should call crypto hash with blake2b512 algo and base64url digest, should memoize return value with hash strategy', async () => { const hashSpy = jest.spyOn(Crypto, 'hash'); + /** + * + */ class Sample { public calls = 0; + /** + * @param {...any} args + */ @memoize({ strategy: 'hash' }) public async run(...args: unknown[]) { this.calls += 1; @@ -151,9 +184,15 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should not memoize return value with hash strategy and different arguments', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param {...any} args + */ @memoize({ strategy: 'hash' }) public async run(...args: unknown[]) { this.calls += 1; @@ -171,9 +210,15 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with hash strategy across several calls with same args', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param arg + */ @memoize({ strategy: 'hash' }) public async run(arg: unknown) { this.calls += 1; @@ -196,9 +241,15 @@ describe('memoize decorator — per-test inline classes', () => { const { memoize: memoizeWithMockedTimers } = await import('../memoize/index'); + /** + * + */ class Sample { public calls = 0; + /** + * @param x + */ @memoizeWithMockedTimers({ strategy: 'concat', ttl: 1_000 }) public async run(x: string) { this.calls += 1; @@ -221,9 +272,15 @@ describe('memoize decorator — per-test inline classes', () => { }); it('error calls should never be momized', async () => { + /** + * + */ class Sample { public calls = 0; + /** + * @param x + */ @memoize() public async run(x: number) { this.calls += 1; @@ -245,9 +302,15 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should NOT cache results listed in skipCache (primitives)', async () => { + /** + * + */ class Sample { public calls = 0; - + + /** + * @param kind + */ @memoize({ strategy: 'concat', skipCache: [null, undefined, 0, false, ''] }) public async run(kind: 'null' | 'undef' | 'zero' | 'false' | 'empty') { this.calls += 1; @@ -260,55 +323,68 @@ describe('memoize decorator — per-test inline classes', () => { } } } - + const sample = new Sample(); - + // Each repeated call should invoke the original again because result is in skipCache. await sample.run('null'); await sample.run('null'); - + await sample.run('undef'); await sample.run('undef'); - + await sample.run('zero'); await sample.run('zero'); - + await sample.run('false'); await sample.run('false'); - + await sample.run('empty'); await sample.run('empty'); - + // 5 kinds × 2 calls each = 10 calls, none cached expect(sample.calls).toBe(10); }); - + it('should cache results NOT listed in skipCache', async () => { + /** + * + */ class Sample { public calls = 0; - + + /** + * @param x + */ @memoize({ strategy: 'concat', skipCache: [null, undefined] }) public async run(x: number) { this.calls += 1; + // returns a non-skipped primitive return x * 2; } } - + const sample = new Sample(); - + expect(await sample.run(21)).toBe(42); expect(await sample.run(21)).toBe(42); - + expect(sample.calls).toBe(1); }); - + it('should use equality for skipCache with objects: deep equal objects are cached', async () => { const deepEqualObject = { a: 1 }; - + + /** + * + */ class Sample { public calls = 0; - + + /** + * + */ @memoize({ strategy: 'concat', skipCache: [deepEqualObject] }) public async run() { this.calls += 1; @@ -316,12 +392,12 @@ describe('memoize decorator — per-test inline classes', () => { return { a: 1 }; } } - + const sample = new Sample(); - + const first = await sample.run(); const second = await sample.run(); - + expect(first).toEqual({ a: 1 }); expect(second).toBe(first); expect(sample.calls).toBe(1); diff --git a/lib/utils/payday.test.ts b/lib/utils/payday.test.ts index beaecd0e..0041ef13 100644 --- a/lib/utils/payday.test.ts +++ b/lib/utils/payday.test.ts @@ -17,9 +17,11 @@ const resetMockedNow = (): void => { // Override Date constructor const RealDate = Date; + global.Date = class extends RealDate { /** * Constructor for mocked Date class + * * @param args - arguments passed to Date constructor */ constructor(...args: unknown[]) { @@ -30,6 +32,9 @@ global.Date = class extends RealDate { } } + /** + * + */ public static now(): number { return mockedNow !== null ? mockedNow : RealDate.now(); } diff --git a/migrations/20250911000000-add-timestamp-index-to-repetitions.js b/migrations/20250911000000-add-timestamp-index-to-repetitions.js index 02509475..5c7b3ba6 100644 --- a/migrations/20250911000000-add-timestamp-index-to-repetitions.js +++ b/migrations/20250911000000-add-timestamp-index-to-repetitions.js @@ -1,84 +1,83 @@ const timestampIndexName = 'timestamp'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/repetitions/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - console.log(`${targetCollections.length} collections will be updated.`); + const targetCollections = []; - let currentCollectionNumber = 1; + collections.forEach((collection) => { + if (/repetitions/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + console.log(`${targetCollections.length} collections will be updated.`); - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - timestamp: 1, - }, { - name: timestampIndexName, - sparse: true, - background: true, - }); - console.log(`Index ${timestampIndexName} created for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); - } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } - currentCollectionNumber++; - } - }, - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; + let currentCollectionNumber = 1; - collections.forEach((collection) => { - if (/repetitions/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} collections will be updated.`); + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - let currentCollectionNumber = 1; + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + timestamp: 1, + }, { + name: timestampIndexName, + sparse: true, + background: true, + }); + console.log(`Index ${timestampIndexName} created for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); + } + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } + currentCollectionNumber++; + } + }, + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + const targetCollections = []; - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(timestampIndexName); - console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); - } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } - currentCollectionNumber++; - } + collections.forEach((collection) => { + if (/repetitions/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - + console.log(`${targetCollections.length} collections will be updated.`); + + let currentCollectionNumber = 1; + + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(timestampIndexName); + console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); + } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } + currentCollectionNumber++; } -} \ No newline at end of file + }, +}; \ No newline at end of file diff --git a/migrations/20250917000000-create-release-project-id-index.js b/migrations/20250917000000-create-release-project-id-index.js index 9fee1934..5f0916cf 100644 --- a/migrations/20250917000000-create-release-project-id-index.js +++ b/migrations/20250917000000-create-release-project-id-index.js @@ -6,12 +6,23 @@ module.exports = { const pairs = await db.collection(collectionName).aggregate([ { $group: { - _id: { projectId: '$projectId', release: '$release' }, + _id: { + projectId: '$projectId', + release: '$release', + }, count: { $sum: 1 }, }, }, - { $project: { _id: 0, projectId: '$_id.projectId', release: '$_id.release', count: 1 } }, - ]).toArray(); + { + $project: { + _id: 0, + projectId: '$_id.projectId', + release: '$_id.release', + count: 1, + }, + }, + ]) + .toArray(); console.log(`Found ${pairs.length} unique (projectId, release) pairs to process.`); @@ -23,7 +34,15 @@ module.exports = { try { const docs = await db.collection(collectionName) - .find({ projectId, release }, { projection: { files: 1, commits: 1 } }) + .find({ + projectId, + release, + }, { + projection: { + files: 1, + commits: 1, + }, + }) .toArray(); const filesByName = new Map(); @@ -56,8 +75,24 @@ module.exports = { * Replace all docs for this pair with a single consolidated doc */ const ops = [ - { deleteMany: { filter: { projectId, release } } }, - { insertOne: { document: { projectId, release, files: mergedFiles, commits: mergedCommits } } }, + { + deleteMany: { + filter: { + projectId, + release, + }, + }, + }, + { + insertOne: { + document: { + projectId, + release, + files: mergedFiles, + commits: mergedCommits, + }, + }, + }, ]; await db.collection(collectionName).bulkWrite(ops, { ordered: true }); @@ -72,10 +107,18 @@ module.exports = { */ try { const hasIndex = await db.collection(collectionName).indexExists(indexName); + if (!hasIndex) { await db.collection(collectionName).createIndex( - { projectId: 1, release: 1 }, - { name: indexName, unique: true, background: true } + { + projectId: 1, + release: 1, + }, + { + name: indexName, + unique: true, + background: true, + } ); console.log(`Index ${indexName} created on ${collectionName} (projectId, release unique).`); } else { @@ -84,13 +127,13 @@ module.exports = { } catch (err) { console.error(`Error creating index ${indexName} on ${collectionName}:`, err); } - }, async down(db) { console.log(`Dropping index ${indexName} from ${collectionName}...`); try { const hasIndex = await db.collection(collectionName).indexExists(indexName); + if (hasIndex) { await db.collection(collectionName).dropIndex(indexName); console.log(`Index ${indexName} dropped from ${collectionName}.`); diff --git a/migrations/20251104000000-add-timestamp-index-to-events.js b/migrations/20251104000000-add-timestamp-index-to-events.js index 83a34d32..c318b7e1 100644 --- a/migrations/20251104000000-add-timestamp-index-to-events.js +++ b/migrations/20251104000000-add-timestamp-index-to-events.js @@ -8,87 +8,87 @@ const timestampIndexName = 'timestamp'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/events:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} events collections will be updated.`); - - let currentCollectionNumber = 1; - - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - timestamp: 1, - }, { - name: timestampIndexName, - sparse: true, - background: true, - }); - console.log(`Index ${timestampIndexName} created for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); - } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } - - currentCollectionNumber++; + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; + + collections.forEach((collection) => { + if (/events:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); + + console.log(`${targetCollections.length} events collections will be updated.`); + + let currentCollectionNumber = 1; + + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + timestamp: 1, + }, { + name: timestampIndexName, + sparse: true, + background: true, + }); + console.log(`Index ${timestampIndexName} created for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); } - }, + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); + currentCollectionNumber++; + } + }, - const targetCollections = []; + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - collections.forEach((collection) => { - if (/events:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + const targetCollections = []; - console.log(`${targetCollections.length} events collections will be updated.`); + collections.forEach((collection) => { + if (/events:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - let currentCollectionNumber = 1; + console.log(`${targetCollections.length} events collections will be updated.`); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + let currentCollectionNumber = 1; - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(timestampIndexName); - console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); - } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - currentCollectionNumber++; + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(timestampIndexName); + console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } + + currentCollectionNumber++; } + }, }; diff --git a/migrations/20251105000000-add-compound-index-to-daily-events.js b/migrations/20251105000000-add-compound-index-to-daily-events.js index 4ffe380d..8140371e 100644 --- a/migrations/20251105000000-add-compound-index-to-daily-events.js +++ b/migrations/20251105000000-add-compound-index-to-daily-events.js @@ -5,90 +5,88 @@ const indexName = 'groupingTimestampAndLastRepetitionTimeAndId'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/dailyEvents:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} dailyEvents collections will be updated.`); - - let currentCollectionNumber = 1; - - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); - - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - groupingTimestamp: -1, - lastRepetitionTime: -1, - _id: -1, - }, { - name: indexName, - background: true, - }); - console.log(`Index ${indexName} created for ${collectionName}`); - } else { - console.log(`Index ${indexName} already exists for ${collectionName}`); - } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } - - currentCollectionNumber++; + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; + + collections.forEach((collection) => { + if (/dailyEvents:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); + + console.log(`${targetCollections.length} dailyEvents collections will be updated.`); + + let currentCollectionNumber = 1; + + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); + + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + groupingTimestamp: -1, + lastRepetitionTime: -1, + _id: -1, + }, { + name: indexName, + background: true, + }); + console.log(`Index ${indexName} created for ${collectionName}`); + } else { + console.log(`Index ${indexName} already exists for ${collectionName}`); } - }, + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); + currentCollectionNumber++; + } + }, - const targetCollections = []; + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - collections.forEach((collection) => { - if (/dailyEvents:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + const targetCollections = []; - console.log(`${targetCollections.length} dailyEvents collections will be updated.`); + collections.forEach((collection) => { + if (/dailyEvents:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - let currentCollectionNumber = 1; + console.log(`${targetCollections.length} dailyEvents collections will be updated.`); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + let currentCollectionNumber = 1; - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(indexName); - console.log(`Index ${indexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${indexName} does not exist for ${collectionName}, skipping drop.`); - } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); - currentCollectionNumber++; + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(indexName); + console.log(`Index ${indexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${indexName} does not exist for ${collectionName}, skipping drop.`); } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } + + currentCollectionNumber++; } + }, }; - - diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index 23fdeb63..c4c243b9 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -301,6 +301,9 @@ export default abstract class SenderWorker extends Worker { })); } + /** + * @param task + */ private async handleBlockedWorkspaceReminderTask(task: SenderWorkerBlockedWorkspaceReminderTask): Promise { const eventType = 'blocked-workspace-reminder'; diff --git a/workers/sentry/src/index.ts b/workers/sentry/src/index.ts index 9ed56c4e..d9e3dde9 100644 --- a/workers/sentry/src/index.ts +++ b/workers/sentry/src/index.ts @@ -41,6 +41,7 @@ export default class SentryEventWorker extends Worker { if (items.length === 0) { this.logger.warn('Received envelope with no items'); + return; } @@ -49,6 +50,7 @@ export default class SentryEventWorker extends Worker { for (const item of items) { const result = await this.handleEnvelopeItem(headers, item, event.projectId); + if (result === 'processed') { processedCount++; } else if (result === 'skipped') { @@ -68,6 +70,8 @@ export default class SentryEventWorker extends Worker { /** * Filter out binary items that crash parseEnvelope * Also filters out all Sentry Replay events (replay_event and replay_recording) + * + * @param rawEvent */ private filterOutBinaryItems(rawEvent: string): string { const lines = rawEvent.split('\n'); @@ -169,6 +173,7 @@ export default class SentryEventWorker extends Worker { */ if (decodedPayload instanceof Uint8Array) { const textDecoder = new TextDecoder(); + decodedPayload = textDecoder.decode(decodedPayload as Uint8Array); } @@ -196,6 +201,7 @@ export default class SentryEventWorker extends Worker { } this.logger.info(`Skipping non-event item of type: ${itemHeader.type}`); + return 'skipped'; } const payloadHasSDK = typeof itemPayload === 'object' && 'sdk' in itemPayload; @@ -245,6 +251,7 @@ export default class SentryEventWorker extends Worker { * If addTask returns false, the message was not queued (queue full or channel closed) */ const error = new Error(`Failed to queue event to ${workerName} worker. Queue may be full or channel closed.`); + this.logger.error(error.message); this.logger.info('👇 Here is the event that failed to queue:'); this.logger.json(hawkEvent); diff --git a/workers/sentry/src/utils/converter.ts b/workers/sentry/src/utils/converter.ts index b3fac849..a5da3c96 100644 --- a/workers/sentry/src/utils/converter.ts +++ b/workers/sentry/src/utils/converter.ts @@ -1,6 +1,49 @@ import { BacktraceFrame, DefaultAddons, EventContext, EventData, Json, SentryAddons } from '@hawk.so/types'; import { Event as SentryEvent } from '@sentry/core'; +/** + * Flattens a nested object into an array of strings using dot notation + * For example: {foo: 1, bar: {baz: 2}} becomes ["foo=1", "bar.baz=2"] + * + * @param obj - The object to flatten + * @param prefix - The prefix to use for nested keys (used in recursion) + */ +function flattenObject(obj: unknown, prefix = ''): string[] { + const result: string[] = []; + + if (obj === null || obj === undefined) { + return [ prefix ? `${prefix}=${obj}` : String(obj) ]; + } + + if (typeof obj !== 'object') { + return [ prefix ? `${prefix}=${obj}` : String(obj) ]; + } + + if (Array.isArray(obj)) { + obj.forEach((value, index) => { + const key = prefix ? `${prefix}.${index}` : String(index); + + result.push(...flattenObject(value, key)); + }); + + return result; + } + + const entries = Object.entries(obj); + + if (entries.length === 0) { + return [ prefix ? `${prefix}={}` : '{}' ]; + } + + entries.forEach(([key, value]) => { + const newPrefix = prefix ? `${prefix}.${key}` : key; + + result.push(...flattenObject(value, newPrefix)); + }); + + return result; +} + /** * Compose title from Sentry event payload * @@ -79,8 +122,8 @@ export function composeBacktrace(eventPayload: SentryEvent): EventData { - return `${name}=${value}`; + backtraceFrame.arguments = Object.entries(frame.vars).flatMap(([name, value]) => { + return flattenObject(value, name); }); } diff --git a/workers/sentry/tests/converter.test.ts b/workers/sentry/tests/converter.test.ts index d84d8afa..e60c2f0f 100644 --- a/workers/sentry/tests/converter.test.ts +++ b/workers/sentry/tests/converter.test.ts @@ -93,6 +93,161 @@ describe('converter utils', () => { }); }); + it('should handle nested objects in vars using dot notation', () => { + const event: SentryEvent = { + exception: { + values: [ { + stacktrace: { + frames: [ { + filename: 'test.js', + lineno: 10, + vars: { + params: { + foo: 1, + bar: 2, + second: { + glass: 3, + }, + }, + }, + } ], + }, + } ], + }, + }; + + const backtrace = composeBacktrace(event); + + expect(backtrace?.[0].arguments).toEqual([ + 'params.foo=1', + 'params.bar=2', + 'params.second.glass=3', + ]); + }); + + it('should handle arrays in vars using dot notation with indices', () => { + const event: SentryEvent = { + exception: { + values: [ { + stacktrace: { + frames: [ { + filename: 'test.js', + lineno: 10, + vars: { + items: ['first', 'second', 'third'], + }, + } ], + }, + } ], + }, + }; + + const backtrace = composeBacktrace(event); + + expect(backtrace?.[0].arguments).toEqual([ + 'items.0=first', + 'items.1=second', + 'items.2=third', + ]); + }); + + it('should handle mixed nested objects and arrays in vars', () => { + const event: SentryEvent = { + exception: { + values: [ { + stacktrace: { + frames: [ { + filename: 'test.js', + lineno: 10, + vars: { + config: { + users: [ + { + name: 'Alice', + age: 30, + }, + { + name: 'Bob', + age: 25, + }, + ], + settings: { + enabled: true, + }, + }, + }, + } ], + }, + } ], + }, + }; + + const backtrace = composeBacktrace(event); + + expect(backtrace?.[0].arguments).toEqual([ + 'config.users.0.name=Alice', + 'config.users.0.age=30', + 'config.users.1.name=Bob', + 'config.users.1.age=25', + 'config.settings.enabled=true', + ]); + }); + + it('should handle null and undefined values in vars', () => { + const event: SentryEvent = { + exception: { + values: [ { + stacktrace: { + frames: [ { + filename: 'test.js', + lineno: 10, + vars: { + nullValue: null, + undefinedValue: undefined, + normalValue: 'test', + }, + } ], + }, + } ], + }, + }; + + const backtrace = composeBacktrace(event); + + expect(backtrace?.[0].arguments).toEqual([ + 'nullValue=null', + 'undefinedValue=undefined', + 'normalValue=test', + ]); + }); + + it('should handle empty objects and arrays in vars', () => { + const event: SentryEvent = { + exception: { + values: [ { + stacktrace: { + frames: [ { + filename: 'test.js', + lineno: 10, + vars: { + emptyObject: {}, + emptyArray: [], + normalValue: 'test', + }, + } ], + }, + } ], + }, + }; + + const backtrace = composeBacktrace(event); + + expect(backtrace?.[0].arguments).toEqual([ + 'emptyObject={}', + 'normalValue=test', + ]); + }); + it('should reverse frames', () => { const event: SentryEvent = { exception: { diff --git a/workers/sentry/tests/index.test.ts b/workers/sentry/tests/index.test.ts index acf44e96..7d964c1b 100644 --- a/workers/sentry/tests/index.test.ts +++ b/workers/sentry/tests/index.test.ts @@ -689,7 +689,7 @@ describe('SentryEventWorker', () => { '__package__=None', '__loader__=<_frozen_importlib_external.SourceFileLoader object at 0x102934cb0>', '__spec__=None', - '__annotations__=[object Object]', + '__annotations__={}', "__builtins__=", "__file__='/Users/nostr/dev/codex/hawk.mono/tests/manual/sentry/sentry-prod.py'", '__cached__=None', From b21ddcd40d350469543634efb0f85c4e7263e4bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:22:21 +0000 Subject: [PATCH 3/4] Fix empty array handling to be consistent with empty objects Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- workers/sentry/src/utils/converter.ts | 4 ++++ workers/sentry/tests/converter.test.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/workers/sentry/src/utils/converter.ts b/workers/sentry/src/utils/converter.ts index a5da3c96..916f158c 100644 --- a/workers/sentry/src/utils/converter.ts +++ b/workers/sentry/src/utils/converter.ts @@ -20,6 +20,10 @@ function flattenObject(obj: unknown, prefix = ''): string[] { } if (Array.isArray(obj)) { + if (obj.length === 0) { + return [ prefix ? `${prefix}=[]` : '[]' ]; + } + obj.forEach((value, index) => { const key = prefix ? `${prefix}.${index}` : String(index); diff --git a/workers/sentry/tests/converter.test.ts b/workers/sentry/tests/converter.test.ts index e60c2f0f..7c630c69 100644 --- a/workers/sentry/tests/converter.test.ts +++ b/workers/sentry/tests/converter.test.ts @@ -244,6 +244,7 @@ describe('converter utils', () => { expect(backtrace?.[0].arguments).toEqual([ 'emptyObject={}', + 'emptyArray=[]', 'normalValue=test', ]); }); From eb368a54033a9aced10dce419cb78a1bd91e6a4c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 19:32:23 +0000 Subject: [PATCH 4/4] Remove unrelated file changes that should not be affected by the solution Co-authored-by: neSpecc <3684889+neSpecc@users.noreply.github.com> --- lib/memoize/index.test.ts | 114 +++----------- lib/utils/payday.test.ts | 5 - ...0000-add-timestamp-index-to-repetitions.js | 141 ++++++++--------- ...7000000-create-release-project-id-index.js | 61 ++------ ...104000000-add-timestamp-index-to-events.js | 142 ++++++++--------- ...0000-add-compound-index-to-daily-events.js | 146 +++++++++--------- workers/sender/src/index.ts | 3 - workers/sentry/src/index.ts | 7 - 8 files changed, 244 insertions(+), 375 deletions(-) diff --git a/lib/memoize/index.test.ts b/lib/memoize/index.test.ts index 705bef95..8953e00b 100644 --- a/lib/memoize/index.test.ts +++ b/lib/memoize/index.test.ts @@ -20,16 +20,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with concat strategy across several calls', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param a - * @param b - */ @memoize({ strategy: 'concat', ttl: 60_000, max: 50 }) public async run(a: number, b: string) { this.calls += 1; @@ -54,16 +47,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with set of arguments with concat strategy across several calls', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param a - * @param b - */ @memoize({ strategy: 'concat' }) public async run(a: unknown, b: unknown) { this.calls += 1; @@ -98,16 +84,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value for stringified objects across several calls', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param x - * @param y - */ @memoize({ strategy: 'concat' }) public async run(x: unknown, y: unknown) { this.calls += 1; @@ -126,15 +105,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value for method with non-default arguments (NaN, Infinity, -0, Symbol, Date, RegExp) still cache same-args', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param {...any} args - */ @memoize({ strategy: 'concat' }) public async run(...args: unknown[]) { this.calls += 1; @@ -158,15 +131,9 @@ describe('memoize decorator — per-test inline classes', () => { it('should call crypto hash with blake2b512 algo and base64url digest, should memoize return value with hash strategy', async () => { const hashSpy = jest.spyOn(Crypto, 'hash'); - /** - * - */ class Sample { public calls = 0; - /** - * @param {...any} args - */ @memoize({ strategy: 'hash' }) public async run(...args: unknown[]) { this.calls += 1; @@ -184,15 +151,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should not memoize return value with hash strategy and different arguments', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param {...any} args - */ @memoize({ strategy: 'hash' }) public async run(...args: unknown[]) { this.calls += 1; @@ -210,15 +171,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should memoize return value with hash strategy across several calls with same args', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param arg - */ @memoize({ strategy: 'hash' }) public async run(arg: unknown) { this.calls += 1; @@ -241,15 +196,9 @@ describe('memoize decorator — per-test inline classes', () => { const { memoize: memoizeWithMockedTimers } = await import('../memoize/index'); - /** - * - */ class Sample { public calls = 0; - /** - * @param x - */ @memoizeWithMockedTimers({ strategy: 'concat', ttl: 1_000 }) public async run(x: string) { this.calls += 1; @@ -272,15 +221,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('error calls should never be momized', async () => { - /** - * - */ class Sample { public calls = 0; - /** - * @param x - */ @memoize() public async run(x: number) { this.calls += 1; @@ -302,15 +245,9 @@ describe('memoize decorator — per-test inline classes', () => { }); it('should NOT cache results listed in skipCache (primitives)', async () => { - /** - * - */ class Sample { public calls = 0; - - /** - * @param kind - */ + @memoize({ strategy: 'concat', skipCache: [null, undefined, 0, false, ''] }) public async run(kind: 'null' | 'undef' | 'zero' | 'false' | 'empty') { this.calls += 1; @@ -323,68 +260,55 @@ describe('memoize decorator — per-test inline classes', () => { } } } - + const sample = new Sample(); - + // Each repeated call should invoke the original again because result is in skipCache. await sample.run('null'); await sample.run('null'); - + await sample.run('undef'); await sample.run('undef'); - + await sample.run('zero'); await sample.run('zero'); - + await sample.run('false'); await sample.run('false'); - + await sample.run('empty'); await sample.run('empty'); - + // 5 kinds × 2 calls each = 10 calls, none cached expect(sample.calls).toBe(10); }); - + it('should cache results NOT listed in skipCache', async () => { - /** - * - */ class Sample { public calls = 0; - - /** - * @param x - */ + @memoize({ strategy: 'concat', skipCache: [null, undefined] }) public async run(x: number) { this.calls += 1; - // returns a non-skipped primitive return x * 2; } } - + const sample = new Sample(); - + expect(await sample.run(21)).toBe(42); expect(await sample.run(21)).toBe(42); - + expect(sample.calls).toBe(1); }); - + it('should use equality for skipCache with objects: deep equal objects are cached', async () => { const deepEqualObject = { a: 1 }; - - /** - * - */ + class Sample { public calls = 0; - - /** - * - */ + @memoize({ strategy: 'concat', skipCache: [deepEqualObject] }) public async run() { this.calls += 1; @@ -392,12 +316,12 @@ describe('memoize decorator — per-test inline classes', () => { return { a: 1 }; } } - + const sample = new Sample(); - + const first = await sample.run(); const second = await sample.run(); - + expect(first).toEqual({ a: 1 }); expect(second).toBe(first); expect(sample.calls).toBe(1); diff --git a/lib/utils/payday.test.ts b/lib/utils/payday.test.ts index 0041ef13..beaecd0e 100644 --- a/lib/utils/payday.test.ts +++ b/lib/utils/payday.test.ts @@ -17,11 +17,9 @@ const resetMockedNow = (): void => { // Override Date constructor const RealDate = Date; - global.Date = class extends RealDate { /** * Constructor for mocked Date class - * * @param args - arguments passed to Date constructor */ constructor(...args: unknown[]) { @@ -32,9 +30,6 @@ global.Date = class extends RealDate { } } - /** - * - */ public static now(): number { return mockedNow !== null ? mockedNow : RealDate.now(); } diff --git a/migrations/20250911000000-add-timestamp-index-to-repetitions.js b/migrations/20250911000000-add-timestamp-index-to-repetitions.js index 5c7b3ba6..02509475 100644 --- a/migrations/20250911000000-add-timestamp-index-to-repetitions.js +++ b/migrations/20250911000000-add-timestamp-index-to-repetitions.js @@ -1,83 +1,84 @@ const timestampIndexName = 'timestamp'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/repetitions/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} collections will be updated.`); - - let currentCollectionNumber = 1; - - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - timestamp: 1, - }, { - name: timestampIndexName, - sparse: true, - background: true, + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; + + collections.forEach((collection) => { + if (/repetitions/.test(collection.name)) { + targetCollections.push(collection.name); + } }); - console.log(`Index ${timestampIndexName} created for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); - } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } - currentCollectionNumber++; - } - }, - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - const targetCollections = []; + console.log(`${targetCollections.length} collections will be updated.`); - collections.forEach((collection) => { - if (/repetitions/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + let currentCollectionNumber = 1; - console.log(`${targetCollections.length} collections will be updated.`); + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - let currentCollectionNumber = 1; + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + timestamp: 1, + }, { + name: timestampIndexName, + sparse: true, + background: true, + }); + console.log(`Index ${timestampIndexName} created for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); + } + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } + currentCollectionNumber++; + } + }, + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + collections.forEach((collection) => { + if (/repetitions/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); + + console.log(`${targetCollections.length} collections will be updated.`); - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + let currentCollectionNumber = 1; - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(timestampIndexName); - console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(timestampIndexName); + console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); + } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } + currentCollectionNumber++; } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } - currentCollectionNumber++; + + } - }, -}; \ No newline at end of file +} \ No newline at end of file diff --git a/migrations/20250917000000-create-release-project-id-index.js b/migrations/20250917000000-create-release-project-id-index.js index 5f0916cf..9fee1934 100644 --- a/migrations/20250917000000-create-release-project-id-index.js +++ b/migrations/20250917000000-create-release-project-id-index.js @@ -6,23 +6,12 @@ module.exports = { const pairs = await db.collection(collectionName).aggregate([ { $group: { - _id: { - projectId: '$projectId', - release: '$release', - }, + _id: { projectId: '$projectId', release: '$release' }, count: { $sum: 1 }, }, }, - { - $project: { - _id: 0, - projectId: '$_id.projectId', - release: '$_id.release', - count: 1, - }, - }, - ]) - .toArray(); + { $project: { _id: 0, projectId: '$_id.projectId', release: '$_id.release', count: 1 } }, + ]).toArray(); console.log(`Found ${pairs.length} unique (projectId, release) pairs to process.`); @@ -34,15 +23,7 @@ module.exports = { try { const docs = await db.collection(collectionName) - .find({ - projectId, - release, - }, { - projection: { - files: 1, - commits: 1, - }, - }) + .find({ projectId, release }, { projection: { files: 1, commits: 1 } }) .toArray(); const filesByName = new Map(); @@ -75,24 +56,8 @@ module.exports = { * Replace all docs for this pair with a single consolidated doc */ const ops = [ - { - deleteMany: { - filter: { - projectId, - release, - }, - }, - }, - { - insertOne: { - document: { - projectId, - release, - files: mergedFiles, - commits: mergedCommits, - }, - }, - }, + { deleteMany: { filter: { projectId, release } } }, + { insertOne: { document: { projectId, release, files: mergedFiles, commits: mergedCommits } } }, ]; await db.collection(collectionName).bulkWrite(ops, { ordered: true }); @@ -107,18 +72,10 @@ module.exports = { */ try { const hasIndex = await db.collection(collectionName).indexExists(indexName); - if (!hasIndex) { await db.collection(collectionName).createIndex( - { - projectId: 1, - release: 1, - }, - { - name: indexName, - unique: true, - background: true, - } + { projectId: 1, release: 1 }, + { name: indexName, unique: true, background: true } ); console.log(`Index ${indexName} created on ${collectionName} (projectId, release unique).`); } else { @@ -127,13 +84,13 @@ module.exports = { } catch (err) { console.error(`Error creating index ${indexName} on ${collectionName}:`, err); } + }, async down(db) { console.log(`Dropping index ${indexName} from ${collectionName}...`); try { const hasIndex = await db.collection(collectionName).indexExists(indexName); - if (hasIndex) { await db.collection(collectionName).dropIndex(indexName); console.log(`Index ${indexName} dropped from ${collectionName}.`); diff --git a/migrations/20251104000000-add-timestamp-index-to-events.js b/migrations/20251104000000-add-timestamp-index-to-events.js index c318b7e1..83a34d32 100644 --- a/migrations/20251104000000-add-timestamp-index-to-events.js +++ b/migrations/20251104000000-add-timestamp-index-to-events.js @@ -8,87 +8,87 @@ const timestampIndexName = 'timestamp'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/events:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} events collections will be updated.`); - - let currentCollectionNumber = 1; - - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - timestamp: 1, - }, { - name: timestampIndexName, - sparse: true, - background: true, - }); - console.log(`Index ${timestampIndexName} created for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; + + collections.forEach((collection) => { + if (/events:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); + + console.log(`${targetCollections.length} events collections will be updated.`); + + let currentCollectionNumber = 1; + + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + timestamp: 1, + }, { + name: timestampIndexName, + sparse: true, + background: true, + }); + console.log(`Index ${timestampIndexName} created for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} already exists for ${collectionName}`); + } + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } + + currentCollectionNumber++; } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } + }, - currentCollectionNumber++; - } - }, + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); + const targetCollections = []; - const targetCollections = []; + collections.forEach((collection) => { + if (/events:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - collections.forEach((collection) => { - if (/events:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + console.log(`${targetCollections.length} events collections will be updated.`); - console.log(`${targetCollections.length} events collections will be updated.`); + let currentCollectionNumber = 1; - let currentCollectionNumber = 1; + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(timestampIndexName); + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(timestampIndexName); + console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); + } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(timestampIndexName); - console.log(`Index ${timestampIndexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${timestampIndexName} does not exist for ${collectionName}, skipping drop.`); + currentCollectionNumber++; } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } - - currentCollectionNumber++; } - }, }; diff --git a/migrations/20251105000000-add-compound-index-to-daily-events.js b/migrations/20251105000000-add-compound-index-to-daily-events.js index 8140371e..4ffe380d 100644 --- a/migrations/20251105000000-add-compound-index-to-daily-events.js +++ b/migrations/20251105000000-add-compound-index-to-daily-events.js @@ -5,88 +5,90 @@ const indexName = 'groupingTimestampAndLastRepetitionTimeAndId'; module.exports = { - async up(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); - - const targetCollections = []; - - collections.forEach((collection) => { - if (/dailyEvents:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); - - console.log(`${targetCollections.length} dailyEvents collections will be updated.`); - - let currentCollectionNumber = 1; - - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); - - if (!hasIndexAlready) { - await db.collection(collectionName).createIndex({ - groupingTimestamp: -1, - lastRepetitionTime: -1, - _id: -1, - }, { - name: indexName, - background: true, - }); - console.log(`Index ${indexName} created for ${collectionName}`); - } else { - console.log(`Index ${indexName} already exists for ${collectionName}`); + async up(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); + + const targetCollections = []; + + collections.forEach((collection) => { + if (/dailyEvents:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); + + console.log(`${targetCollections.length} dailyEvents collections will be updated.`); + + let currentCollectionNumber = 1; + + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); + + if (!hasIndexAlready) { + await db.collection(collectionName).createIndex({ + groupingTimestamp: -1, + lastRepetitionTime: -1, + _id: -1, + }, { + name: indexName, + background: true, + }); + console.log(`Index ${indexName} created for ${collectionName}`); + } else { + console.log(`Index ${indexName} already exists for ${collectionName}`); + } + } catch (error) { + console.error(`Error adding index to ${collectionName}:`, error); + } + + currentCollectionNumber++; } - } catch (error) { - console.error(`Error adding index to ${collectionName}:`, error); - } + }, - currentCollectionNumber++; - } - }, + async down(db) { + const collections = await db.listCollections({}, { + authorizedCollections: true, + nameOnly: true, + }).toArray(); - async down(db) { - const collections = await db.listCollections({}, { - authorizedCollections: true, - nameOnly: true, - }).toArray(); + const targetCollections = []; - const targetCollections = []; + collections.forEach((collection) => { + if (/dailyEvents:/.test(collection.name)) { + targetCollections.push(collection.name); + } + }); - collections.forEach((collection) => { - if (/dailyEvents:/.test(collection.name)) { - targetCollections.push(collection.name); - } - }); + console.log(`${targetCollections.length} dailyEvents collections will be updated.`); - console.log(`${targetCollections.length} dailyEvents collections will be updated.`); + let currentCollectionNumber = 1; - let currentCollectionNumber = 1; + for (const collectionName of targetCollections) { + console.log(`${collectionName} in process.`); + console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); - for (const collectionName of targetCollections) { - console.log(`${collectionName} in process.`); - console.log(`${currentCollectionNumber} of ${targetCollections.length} in process.`); + try { + const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); - try { - const hasIndexAlready = await db.collection(collectionName).indexExists(indexName); + if (hasIndexAlready) { + await db.collection(collectionName).dropIndex(indexName); + console.log(`Index ${indexName} dropped for ${collectionName}`); + } else { + console.log(`Index ${indexName} does not exist for ${collectionName}, skipping drop.`); + } + } catch (error) { + console.error(`Error dropping index from ${collectionName}:`, error); + } - if (hasIndexAlready) { - await db.collection(collectionName).dropIndex(indexName); - console.log(`Index ${indexName} dropped for ${collectionName}`); - } else { - console.log(`Index ${indexName} does not exist for ${collectionName}, skipping drop.`); + currentCollectionNumber++; } - } catch (error) { - console.error(`Error dropping index from ${collectionName}:`, error); - } - - currentCollectionNumber++; } - }, }; + + diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index c4c243b9..23fdeb63 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -301,9 +301,6 @@ export default abstract class SenderWorker extends Worker { })); } - /** - * @param task - */ private async handleBlockedWorkspaceReminderTask(task: SenderWorkerBlockedWorkspaceReminderTask): Promise { const eventType = 'blocked-workspace-reminder'; diff --git a/workers/sentry/src/index.ts b/workers/sentry/src/index.ts index d9e3dde9..9ed56c4e 100644 --- a/workers/sentry/src/index.ts +++ b/workers/sentry/src/index.ts @@ -41,7 +41,6 @@ export default class SentryEventWorker extends Worker { if (items.length === 0) { this.logger.warn('Received envelope with no items'); - return; } @@ -50,7 +49,6 @@ export default class SentryEventWorker extends Worker { for (const item of items) { const result = await this.handleEnvelopeItem(headers, item, event.projectId); - if (result === 'processed') { processedCount++; } else if (result === 'skipped') { @@ -70,8 +68,6 @@ export default class SentryEventWorker extends Worker { /** * Filter out binary items that crash parseEnvelope * Also filters out all Sentry Replay events (replay_event and replay_recording) - * - * @param rawEvent */ private filterOutBinaryItems(rawEvent: string): string { const lines = rawEvent.split('\n'); @@ -173,7 +169,6 @@ export default class SentryEventWorker extends Worker { */ if (decodedPayload instanceof Uint8Array) { const textDecoder = new TextDecoder(); - decodedPayload = textDecoder.decode(decodedPayload as Uint8Array); } @@ -201,7 +196,6 @@ export default class SentryEventWorker extends Worker { } this.logger.info(`Skipping non-event item of type: ${itemHeader.type}`); - return 'skipped'; } const payloadHasSDK = typeof itemPayload === 'object' && 'sdk' in itemPayload; @@ -251,7 +245,6 @@ export default class SentryEventWorker extends Worker { * If addTask returns false, the message was not queued (queue full or channel closed) */ const error = new Error(`Failed to queue event to ${workerName} worker. Queue may be full or channel closed.`); - this.logger.error(error.message); this.logger.info('👇 Here is the event that failed to queue:'); this.logger.json(hawkEvent);