Skip to content

Commit

Permalink
edge-api-sdk: add more info to sentry events
Browse files Browse the repository at this point in the history
  • Loading branch information
joshbalfour committed Apr 11, 2024
1 parent 0f53b1c commit f4f9d44
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 61 deletions.
2 changes: 1 addition & 1 deletion packages/modules/edge-api-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"description": "Provides convenience wrappers for accepting and responding to [@reapit-cdk/edge-api]('../../constructs/edge-api/readme.md') lambda requests.",
"version": "0.0.15",
"version": "0.0.16",
"homepage": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/modules/edge-api-sdk",
"readme": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/modules/edge-api-sdk/readme.md",
"bugs": {
Expand Down
74 changes: 62 additions & 12 deletions packages/modules/edge-api-sdk/src/sentry/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,53 @@ import {
import { SeverityLevel } from '@sentry/types'

import { LogLevel, LogPayload, MinimalLogPayload, Transport } from '../logger'
import { RCHeaders, RCQuery, RCRequest } from '../types'

const stackParser = stackParserFromStackParserOptions(
createStackParser(nodeStackLineParser(createGetModuleFromFilename())),
)

export type InitProps = {
dsn: string
environment?: string
release?: string
sessionId?: string
}

const logLevelToSeverityLevel = (logLevel: LogLevel): SeverityLevel => {
if (logLevel === 'panic' || logLevel === 'critical') {
return 'fatal'
}
return logLevel
}

export const init = (props: InitProps): Transport => {
const components = dsnFromString(props.dsn)
export const getCookies = (cookie: string): Record<string, string> =>
Object.fromEntries(
cookie
.split('; ')
.map((v) => {
try {
return v.split(/=(.*)/s).map(decodeURIComponent)
} catch {
return ['', '']
}
})
.filter(([v]) => !!v),
)

const pickFirst = (queryOrHeaders: RCQuery | RCHeaders) => {
return Object.entries(queryOrHeaders)
.map(([k, v]) => [k, Array.isArray(v) ? v[0] : v])
.reduce((pv, cv) => {
return {
...pv,
[cv[0]]: cv[1],
}
}, {})
}

export const init = (request: RCRequest<{ sentryDsn: string; sentryRelease: string }>): Transport => {
const {
host,
cookies,
env: { sentryDsn, sentryRelease },
region,
meta: { sessionId, functionName, functionVersion, invocationId },
} = request
const components = dsnFromString(sentryDsn)
if (!components) {
throw new Error('invalid DSN provided')
}
Expand All @@ -49,10 +75,34 @@ export const init = (props: InitProps): Transport => {
: undefined,
timestamp: errorEntry?.timestamp.getTime(),
tags: {
sessionId: props.sessionId,
sessionId,
functionName,
},
contexts: {
invocation: {
functionName,
functionVersion,
invocationId,
region,
},
},
request: {
headers: pickFirst(request.headers),
method: request.method,
query_string: request.query ? pickFirst(request.query) : undefined,
cookies: cookies.map(getCookies).reduce(
(pv, cv) => {
return {
...pv,
...cv,
}
},
{} as Record<string, string>,
),
url: request.path,
},
release: props.release,
environment: props.environment,
release: sentryRelease,
environment: host,
breadcrumbs: payload.entries.map((entry) => {
return {
level: logLevelToSeverityLevel(entry.level),
Expand Down
135 changes: 87 additions & 48 deletions packages/modules/edge-api-sdk/tests/sentry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,24 @@ import { JSONRequest } from '../src'
import { init } from '../src/sentry/sentry'
import { generateCloudfrontRequest } from './logger.test'
import { LogEntry, LogPayload } from '../src/logger'
import { Event } from '@sentry/types'

const generateLogPayload = (entries: LogEntry[]): LogPayload => {
const event = generateCloudfrontRequest({})
const request: JSONRequest<any, any> = {
env: {},
env: {
sentryDsn: 'https://asdf@qwerty.ingest.sentry.io/123456',
sentryRelease: 'sentry-release',
},
headers: {
host: ['google.com'],
},
method: 'GET',
host: 'google.com',
path: '/',
query: {
a: 'b',
},
region: 'eu-west-2',
cookies: [],
body: {},
Expand All @@ -33,13 +40,13 @@ const generateLogPayload = (entries: LogEntry[]): LogPayload => {
centralize: true,
entries,
event,
functionName: 'functionName',
functionVersion: 'functionVersion',
invocationId: '',
region: '',
functionName: 'function-name',
functionVersion: 'function-version',
invocationId: 'invocation-id',
region: 'eu-west-2',
request,
timestamp: new Date(0),
sessionId: 'sessionId',
sessionId: 'session-id',
}
}

Expand All @@ -48,30 +55,22 @@ describe('sentry logger transport', () => {
fetchMock.resetMocks()
})
it('should init', () => {
init({
dsn: 'https://asdf@qwerty.ingest.sentry.io/123456',
environment: 'environment',
release: 'release',
})
const { request } = generateLogPayload([])
init(request)
})

it('should send exceptions to sentry', async () => {
const report = init({
dsn: 'https://asdf@qwerty.ingest.sentry.io/123456',
environment: 'environment',
release: 'release',
})
const payload = generateLogPayload([
{
level: 'critical',
message: 'the error message',
timestamp: new Date(0),
error: new Error('oh no'),
},
])
const report = init(payload.request)

await report(
generateLogPayload([
{
level: 'critical',
message: 'the error message',
timestamp: new Date(0),
error: new Error('oh no'),
},
]),
)
await report(payload)

const [endpoint, reqInit] = fetchMock.mock.calls[0]
expect(endpoint).toBe('https://qwerty.ingest.sentry.io/api/123456/envelope/?sentry_key=asdf&sentry_version=7')
Expand All @@ -84,32 +83,72 @@ describe('sentry logger transport', () => {
})

it('should send prior log entries as breadcrumbs', async () => {
const report = init({
dsn: 'https://asdf@qwerty.ingest.sentry.io/123456',
environment: 'environment',
release: 'release',
})
const payload = generateLogPayload([
{
level: 'info',
message: 'something interesting here',
timestamp: new Date(0),
},
{
level: 'critical',
message: 'the error message',
timestamp: new Date(1),
error: new Error('oh no'),
},
])
const report = init(payload.request)

await report(
generateLogPayload([
{
level: 'info',
message: 'something interesting here',
timestamp: new Date(0),
},
{
level: 'critical',
message: 'the error message',
timestamp: new Date(1),
error: new Error('oh no'),
},
]),
)
await report(payload)
const [, reqInit] = fetchMock.mock.calls[0]
const [, , exception] = (reqInit?.body as string)?.split('\n').map((str) => JSON.parse(str)) ?? []
expect(exception.breadcrumbs).toEqual([
const [, , event] = (reqInit?.body as string)?.split('\n').map((str) => JSON.parse(str)) ?? []
const ev = event as Event
expect(ev.breadcrumbs).toEqual([
{ level: 'info', message: 'something interesting here', timestamp: 0 },
{ level: 'fatal', message: 'the error message', timestamp: 1 },
])
})

it('should add contextual information to the event', async () => {
const payload = generateLogPayload([
{
level: 'info',
message: 'something interesting here',
timestamp: new Date(0),
},
{
level: 'critical',
message: 'the error message',
timestamp: new Date(1),
error: new Error('oh no'),
},
])
const report = init(payload.request)
await report(payload)
const [, reqInit] = fetchMock.mock.calls[0]
const [, , event] = (reqInit?.body as string)?.split('\n').map((str) => JSON.parse(str)) ?? []
const ev = event as Event
expect(ev.environment).toBe('google.com')
expect(ev.tags).toEqual({
sessionId: 'session-id',
functionName: 'function-name',
})
expect(ev.release).toBe('sentry-release')
expect(ev.contexts?.invocation).toEqual({
functionName: 'function-name',
functionVersion: 'function-version',
invocationId: 'invocation-id',
region: 'eu-west-2',
})
expect(ev.request).toEqual({
headers: {
host: 'google.com',
},
method: 'GET',
query_string: {
a: 'b',
},
cookies: {},
url: '/',
})
})
})

0 comments on commit f4f9d44

Please sign in to comment.