Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion jest.setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const mockedAmqpChannel = {
close: jest.fn(),
assertQueue: jest.fn(),
prefetch: jest.fn(),
sendToQueue: jest.fn(),
sendToQueue: jest.fn().mockReturnValue(true),
on: jest.fn(),
consume: jest.fn().mockReturnValue('mockedTag'),
};
Expand Down
4 changes: 3 additions & 1 deletion lib/worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,9 @@ export abstract class Worker {
console.log('handle error');
console.log(e);

HawkCatcher.send(e, context);
HawkCatcher.send(e, Object.assign(context, {
worker: this.type,
}));

switch (e.constructor) {
case CriticalError:
Expand Down
92 changes: 80 additions & 12 deletions workers/sentry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,24 @@ export default class SentryEventWorker extends Worker {

const [headers, items] = envelope;

if (items.length === 0) {
this.logger.warn('Received envelope with no items');
return;
}

let processedCount = 0;
let skippedCount = 0;

for (const item of items) {
await this.handleEnvelopeItem(headers, item, event.projectId);
const result = await this.handleEnvelopeItem(headers, item, event.projectId);
if (result === 'processed') {
processedCount++;
} else if (result === 'skipped') {
skippedCount++;
}
}

this.logger.verbose(`Processed ${processedCount} events, skipped ${skippedCount} non-event items from envelope`);
} catch (error) {
this.logger.error(`Error handling Sentry event task:`, error);
this.logger.info('👇 Here is the problematic event:');
Expand Down Expand Up @@ -99,8 +114,9 @@ export default class SentryEventWorker extends Worker {
* @param envelopeHeaders - The whole envelope headers
* @param item - Sentry item
* @param projectId - Sentry project ID
* @returns 'processed' if event was sent, 'skipped' if non-event item, throws error on failure
*/
private async handleEnvelopeItem(envelopeHeaders: Envelope[0], item: EnvelopeItem, projectId: string): Promise<void> {
private async handleEnvelopeItem(envelopeHeaders: Envelope[0], item: EnvelopeItem, projectId: string): Promise<'processed' | 'skipped'> {
try {
const [itemHeader, itemPayload] = item;

Expand All @@ -112,7 +128,8 @@ export default class SentryEventWorker extends Worker {
* Skip non-event items
*/
if (itemHeader.type !== 'event') {
return;
this.logger.info(`Skipping non-event item of type: ${itemHeader.type}`);
return 'skipped';
}
const payloadHasSDK = typeof itemPayload === 'object' && 'sdk' in itemPayload;

Expand All @@ -121,18 +138,53 @@ export default class SentryEventWorker extends Worker {
*/
const sentryJsSDK = ['browser', 'react', 'vue', 'angular', 'capacirtor', 'electron'];

const isJsSDK = payloadHasSDK && sentryJsSDK.includes(itemPayload.sdk.name);
/**
* Safely check if SDK name exists and is in the list
* SDK name can be either a simple name like "react" or a full name like "sentry.javascript.react"
*/
const sdkName = payloadHasSDK && itemPayload.sdk && typeof itemPayload.sdk === 'object' && 'name' in itemPayload.sdk
? itemPayload.sdk.name
: undefined;

/**
* Check if SDK is a JavaScript-related SDK
* Supports both simple names (e.g., "react") and full names (e.g., "sentry.javascript.react")
*/
const isJsSDK = sdkName !== undefined && typeof sdkName === 'string' && (
/**
* Exact match for simple SDK names (e.g., "react", "browser")
*/
sentryJsSDK.includes(sdkName) ||
/**
* Check if SDK name contains one of the JS SDK names
* Examples:
* - "sentry.javascript.react" matches "react"
* - "sentry.javascript.browser" matches "browser"
* - "@sentry/react" matches "react"
*/
sentryJsSDK.some((jsSDK) => sdkName.includes(jsSDK))
);

const hawkEvent = this.transformToHawkFormat(envelopeHeaders as EventEnvelope[0], item as EventItem, projectId, isJsSDK);

/**
* If we have release attached to the event
* Send task to appropriate worker and check if it was successfully queued
*/
if (isJsSDK) {
await this.addTask(WorkerNames.JAVASCRIPT, hawkEvent as JavaScriptEventWorkerTask);
} else {
await this.addTask(WorkerNames.DEFAULT, hawkEvent as DefaultEventWorkerTask);
const workerName = isJsSDK ? WorkerNames.JAVASCRIPT : WorkerNames.DEFAULT;
const taskSent = await this.addTask(workerName, hawkEvent as JavaScriptEventWorkerTask | DefaultEventWorkerTask);

if (!taskSent) {
/**
* 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);
throw error;
}

return 'processed';
} catch (error) {
this.logger.error('Error handling envelope item:', error);
this.logger.info('👇 Here is the problematic item:');
Expand Down Expand Up @@ -162,7 +214,14 @@ export default class SentryEventWorker extends Worker {
* convert sent_at from ISO 8601 to Unix timestamp
*/
const msInSecond = 1000;
const sentAtUnix = Math.floor(new Date(sent_at).getTime() / msInSecond);
const sentAtDate = new Date(sent_at);
const sentAtTime = sentAtDate.getTime();

if (isNaN(sentAtTime)) {
throw new Error(`Invalid sent_at timestamp: ${sent_at}`);
}

const sentAtUnix = Math.floor(sentAtTime / msInSecond);
/* eslint-enable @typescript-eslint/naming-convention */

// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
Expand All @@ -175,9 +234,18 @@ export default class SentryEventWorker extends Worker {
* We need to decode it to JSON
*/
if (eventPayload instanceof Uint8Array) {
const textDecoder = new TextDecoder();
try {
const textDecoder = new TextDecoder();
const decoded = textDecoder.decode(eventPayload as Uint8Array);

eventPayload = JSON.parse(textDecoder.decode(eventPayload as Uint8Array));
try {
eventPayload = JSON.parse(decoded);
} catch (parseError) {
throw new Error(`Failed to parse event payload JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
}
} catch (decodeError) {
throw new Error(`Failed to decode Uint8Array event payload: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}`);
}
}

const title = composeTitle(eventPayload);
Expand Down
Loading