diff --git a/workers/sentry/src/utils/converter.ts b/workers/sentry/src/utils/converter.ts index b3fac849..916f158c 100644 --- a/workers/sentry/src/utils/converter.ts +++ b/workers/sentry/src/utils/converter.ts @@ -1,6 +1,53 @@ 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)) { + if (obj.length === 0) { + return [ prefix ? `${prefix}=[]` : '[]' ]; + } + + 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 +126,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..7c630c69 100644 --- a/workers/sentry/tests/converter.test.ts +++ b/workers/sentry/tests/converter.test.ts @@ -93,6 +93,162 @@ 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={}', + 'emptyArray=[]', + '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',