From dd846a3c4ed8d6b6255db69c6dfaef8cf8061d1b Mon Sep 17 00:00:00 2001 From: Josh Balfour Date: Tue, 9 Apr 2024 15:08:33 +0100 Subject: [PATCH] massively reduce sentry dep size --- packages/modules/edge-api-sdk/package.json | 2 +- packages/modules/edge-api-sdk/src/index.ts | 2 +- packages/modules/edge-api-sdk/src/sentry.ts | 59 ---------- .../modules/edge-api-sdk/src/sentry/module.ts | 57 +++++++++ .../modules/edge-api-sdk/src/sentry/sentry.ts | 108 ++++++++++++++++++ 5 files changed, 167 insertions(+), 61 deletions(-) delete mode 100644 packages/modules/edge-api-sdk/src/sentry.ts create mode 100644 packages/modules/edge-api-sdk/src/sentry/module.ts create mode 100644 packages/modules/edge-api-sdk/src/sentry/sentry.ts diff --git a/packages/modules/edge-api-sdk/package.json b/packages/modules/edge-api-sdk/package.json index a9159d7..b0b818e 100644 --- a/packages/modules/edge-api-sdk/package.json +++ b/packages/modules/edge-api-sdk/package.json @@ -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.9", + "version": "0.0.11", "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": { diff --git a/packages/modules/edge-api-sdk/src/index.ts b/packages/modules/edge-api-sdk/src/index.ts index 1b9b630..024f23e 100644 --- a/packages/modules/edge-api-sdk/src/index.ts +++ b/packages/modules/edge-api-sdk/src/index.ts @@ -1,4 +1,4 @@ export * from './edge-api-sdk' export * from './types' export * from './config' -export * from './sentry' +export * from './sentry/sentry' diff --git a/packages/modules/edge-api-sdk/src/sentry.ts b/packages/modules/edge-api-sdk/src/sentry.ts deleted file mode 100644 index cabff15..0000000 --- a/packages/modules/edge-api-sdk/src/sentry.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NodeClient, createTransport, createGetModuleFromFilename } from '@sentry/node' -import { NodeTransportOptions } from '@sentry/node/types/transports' -import { Transport as SentryTransport, SeverityLevel } from '@sentry/types' -import { stackParserFromStackParserOptions, createStackParser, nodeStackLineParser } from '@sentry/utils' -import { LogLevel, LogPayload, MinimalLogPayload, Transport } from './logger' - -const sentryTransport: (transportOptions: NodeTransportOptions) => SentryTransport = (options) => { - return createTransport(options, async (request) => { - const response = await fetch(options.url, { - body: request.body, - method: 'POST', - referrerPolicy: 'origin', - headers: options.headers, - }) - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - } - }) -} - -const stackParser = stackParserFromStackParserOptions( - createStackParser(nodeStackLineParser(createGetModuleFromFilename())), -) - -export type InitProps = { - dsn: string - environment?: string - release?: string -} - -const logLevelToSeverityLevel = (logLevel: LogLevel): SeverityLevel => { - if (logLevel === 'panic' || logLevel === 'critical') { - return 'fatal' - } - return logLevel -} - -export const init = (props: InitProps): Transport => { - const client = new NodeClient({ - integrations: [], - transport: sentryTransport, - stackParser, - ...props, - }) - - return async (payload: MinimalLogPayload | LogPayload) => { - payload.entries.forEach((entry) => { - client.captureMessage(entry.message, logLevelToSeverityLevel(entry.level)) - if (entry.error) { - client.captureException(entry.error) - } - }) - await client.flush() - } -} diff --git a/packages/modules/edge-api-sdk/src/sentry/module.ts b/packages/modules/edge-api-sdk/src/sentry/module.ts new file mode 100644 index 0000000..1f72f12 --- /dev/null +++ b/packages/modules/edge-api-sdk/src/sentry/module.ts @@ -0,0 +1,57 @@ +import { posix, sep } from 'path' +import { dirname } from '@sentry/utils' + +/** normalizes Windows paths */ +function normalizeWindowsPath(path: string): string { + return path + .replace(/^[A-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\` instances with `/` +} + +/** Creates a function that gets the module name from a filename */ +export function createGetModuleFromFilename( + basePath: string = process.argv[1] ? dirname(process.argv[1]) : process.cwd(), + isWindows: boolean = sep === '\\', +): (filename: string | undefined) => string | undefined { + const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath + + return (filename: string | undefined) => { + if (!filename) { + return + } + + const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename + + // eslint-disable-next-line prefer-const + let { dir, base: file, ext } = posix.parse(normalizedFilename) + + if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { + file = file.slice(0, ext.length * -1) + } + + if (!dir) { + // No dirname whatsoever + dir = '.' + } + + const n = dir.lastIndexOf('/node_modules') + if (n > -1) { + return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}` + } + + // Let's see if it's a part of the main module + // To be a part of main module, it has to share the same base + if (dir.startsWith(normalizedBase)) { + let moduleName = dir.slice(normalizedBase.length + 1).replace(/\//g, '.') + + if (moduleName) { + moduleName += ':' + } + moduleName += file + + return moduleName + } + + return file + } +} diff --git a/packages/modules/edge-api-sdk/src/sentry/sentry.ts b/packages/modules/edge-api-sdk/src/sentry/sentry.ts new file mode 100644 index 0000000..a307f84 --- /dev/null +++ b/packages/modules/edge-api-sdk/src/sentry/sentry.ts @@ -0,0 +1,108 @@ +import { createGetModuleFromFilename } from './module' + +import { createEventEnvelope, getEnvelopeEndpointWithUrlEncodedAuth } from '@sentry/core' +import { + stackParserFromStackParserOptions, + createStackParser, + nodeStackLineParser, + exceptionFromError, + dsnFromString, +} from '@sentry/utils' + +import { Envelope, SeverityLevel } from '@sentry/types' + +import { LogLevel, LogPayload, MinimalLogPayload, Transport } from '../logger' + +const encodeUTF8 = (input: string) => new TextEncoder().encode(input) + +export function serializeEnvelope(envelope: Envelope): string | Uint8Array { + const [envHeaders, items] = envelope + + // Initially we construct our envelope as a string and only convert to binary chunks if we encounter binary data + let parts: string | Uint8Array[] = JSON.stringify(envHeaders) + + function append(next: string | Uint8Array): void { + if (typeof parts === 'string') { + parts = typeof next === 'string' ? parts + next : [encodeUTF8(parts), next] + } else { + parts.push(typeof next === 'string' ? encodeUTF8(next) : next) + } + } + + for (const item of items) { + const [itemHeaders, payload] = item + + append(`\n${JSON.stringify(itemHeaders)}\n`) + + if (typeof payload === 'string' || payload instanceof Uint8Array) { + append(payload) + } else { + append(JSON.stringify(payload)) + } + } + + return typeof parts === 'string' ? parts : concatBuffers(parts) +} + +function concatBuffers(buffers: Uint8Array[]): Uint8Array { + const totalLength = buffers.reduce((acc, buf) => acc + buf.length, 0) + + const merged = new Uint8Array(totalLength) + let offset = 0 + for (const buffer of buffers) { + merged.set(buffer, offset) + offset += buffer.length + } + + return merged +} + +const stackParser = stackParserFromStackParserOptions( + createStackParser(nodeStackLineParser(createGetModuleFromFilename())), +) + +export type InitProps = { + dsn: string + environment?: string + release?: 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) + if (!components) { + throw new Error('invalid DSN provided') + } + + return async (payload: MinimalLogPayload | LogPayload) => { + const errorEntry = payload.entries.find((entry) => !!entry.error) + const exception = errorEntry?.error ? exceptionFromError(stackParser, errorEntry.error) : undefined + const envelope = createEventEnvelope({ + exception: exception + ? { + values: [exception], + } + : undefined, + timestamp: errorEntry?.timestamp.getTime(), + breadcrumbs: payload.entries.map((entry) => { + return { + level: logLevelToSeverityLevel(entry.level), + message: entry.message, + timestamp: entry.timestamp.getTime(), + } + }), + }) + + const endpoint = getEnvelopeEndpointWithUrlEncodedAuth(components) + await fetch(endpoint, { + body: serializeEnvelope(envelope), + method: 'post', + }) + } +}