diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 7c997a3de32f..dc77f7d5f6b9 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -109,6 +109,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index d5aabe372bd0..cd302d5523f0 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -129,6 +129,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, processSessionIntegration, prismaIntegration, openTelemetryIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index aa7b2cfc74bf..c0cc191e2850 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -150,6 +150,7 @@ export { tediousIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index dd23caea66cc..c47dc1628c74 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -156,6 +156,7 @@ export { typesafeIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, tediousIntegration, eveConversationHook, eveIntegration, diff --git a/packages/elysia/src/index.ts b/packages/elysia/src/index.ts index 659cb35cf870..865ace1fc978 100644 --- a/packages/elysia/src/index.ts +++ b/packages/elysia/src/index.ts @@ -129,6 +129,7 @@ export { tediousIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, processSessionIntegration, hapiIntegration, diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 828f77501a48..5bbf486e2342 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -129,6 +129,7 @@ export { pinoIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, openTelemetryIntegration, getOtlpTracesEndpoint, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 0dbf9dc692b9..df5d309ee9e6 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -38,6 +38,7 @@ export { typesafeIntegration, postgresIntegration, postgresJsIntegration, + neonIntegration, redisIntegration, tediousIntegration, vercelAIIntegration, diff --git a/packages/server-utils/src/index.ts b/packages/server-utils/src/index.ts index 22b7470fa7b7..41eb3845cf1a 100644 --- a/packages/server-utils/src/index.ts +++ b/packages/server-utils/src/index.ts @@ -68,6 +68,7 @@ export { mysql2Integration } from './integrations/mysql2'; export { openAIIntegration } from './integrations/openai'; export { postgresIntegration } from './integrations/postgres'; export { postgresJsIntegration } from './integrations/postgres-js'; +export { neonIntegration } from './integrations/neon'; export { tediousIntegration } from './integrations/tedious'; export { vercelAIIntegration } from './integrations/vercel-ai'; export { expressIntegration } from './integrations/express'; diff --git a/packages/server-utils/src/integrations/index.ts b/packages/server-utils/src/integrations/index.ts index a09d590e1121..5b044dd29430 100644 --- a/packages/server-utils/src/integrations/index.ts +++ b/packages/server-utils/src/integrations/index.ts @@ -27,6 +27,7 @@ import { groqIntegration } from './groq'; import { togetherAIIntegration } from './together-ai'; import { typesafeIntegration } from './typesafe'; import { postgresJsIntegration } from './postgres-js'; +import { neonIntegration } from './neon'; import { firebaseIntegration } from './firebase'; import { expressIntegration } from './express'; import { fastifyIntegration } from './fastify'; @@ -70,6 +71,7 @@ export function getTracingIntegrations(): Integration[] { togetherAIIntegration(), typesafeIntegration(), postgresJsIntegration(), + neonIntegration(), firebaseIntegration(), mcpServerIntegration(), ]; diff --git a/packages/server-utils/src/integrations/neon.ts b/packages/server-utils/src/integrations/neon.ts new file mode 100644 index 000000000000..f7e66ff953a2 --- /dev/null +++ b/packages/server-utils/src/integrations/neon.ts @@ -0,0 +1,158 @@ +import * as diagnosticsChannel from '../utils/diagnosticsChannel'; +import { + DB_OPERATION_BATCH_SIZE, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { IntegrationFn, Span } from '@sentry/core'; +import { defineIntegration, getActiveSpan, startInactiveSpan } from '@sentry/core'; +import { sanitizeSqlQueryWithSummary } from '../utils/sql'; +import { CHANNELS } from '../orchestrion/channels'; +import { neonModuleNames } from '../orchestrion/config/neon'; +import { invokeOrchestrionInstrumentation } from '../orchestrion/instrumentation'; +import { bindTracingChannelToSpan } from '../tracing-channel'; +import { bindPgQueryChannel, getConnectionAttributes } from './postgres'; + +const INTEGRATION_NAME = 'Neon' as const; +const ORIGIN = 'auto.db.neon'; +const DB_SYSTEM_POSTGRESQL = 'postgresql'; +const DEFAULT_PORT = 5432; +const NOOP = (): void => {}; + +// `sql.query(text, params)` builds the first shape, a tagged-template call the second. +type NeonQueryData = { query: string; params?: unknown[] } | NeonSqlTemplate; + +interface NeonSqlTemplate { + strings: readonly string[]; + values: unknown[]; + toParameterizedQuery?: () => { query: string }; +} + +interface NeonHttpQueryContext { + // `[queryData, opts]` for a single query, `[queryData[], opts[]]` for a `transaction()` batch. + arguments: unknown[]; +} + +interface NeonResolveConnectionContext { + result?: { resolvedURL?: URL }; +} + +// The HTTP query spans this integration started, so the connection-resolver subscriber only +// enriches those and never a user's own active span. +const httpQuerySpans = new WeakSet(); + +const _neonIntegration = (() => { + return { + name: INTEGRATION_NAME, + setup(client) { + invokeOrchestrionInstrumentation(client, neonModuleNames, instrumentNeon, []); + }, + }; +}) satisfies IntegrationFn; + +function instrumentNeon(): void { + // The WebSocket driver is pg's `Client`, bundled into the package, so its query channel gets + // the pg span shape. + bindPgQueryChannel(CHANNELS.NEON_QUERY, { origin: ORIGIN }); + + bindTracingChannelToSpan( + diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), + data => { + const span = startInactiveSpan(httpQuerySpanOptions(data.arguments)); + httpQuerySpans.add(span); + return span; + }, + { requiresParentSpan: true }, + ); + + // The connection string is resolved inside the HTTP executor, so the query span is active + // when this settles. The executor's own context never sees the string. + diagnosticsChannel.tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).subscribe({ + start: NOOP, + end: NOOP, + asyncStart: NOOP, + error: NOOP, + asyncEnd(data) { + const span = getActiveSpan(); + const url = data.result?.resolvedURL; + if (!span || !httpQuerySpans.has(span) || !url) { + return; + } + span.setAttributes( + getConnectionAttributes({ + host: url.hostname, + port: Number(url.port) || DEFAULT_PORT, + database: url.pathname.slice(1), + user: url.username || undefined, + }), + ); + }, + }); +} + +function httpQuerySpanOptions(args: unknown[]): Parameters[0] { + const queryData = args[0] as NeonQueryData | NeonQueryData[] | undefined; + const batchSize = Array.isArray(queryData) ? queryData.length : undefined; + // A batch gets one span whose text and summary list every statement, in order. + const statements = (Array.isArray(queryData) ? queryData : [queryData]) + .map(getQueryText) + .filter((text): text is string => !!text) + .map(text => sanitizeSqlQueryWithSummary(text)); + const queryText = joinDefined(statements.map(s => s.queryText)); + const querySummary = joinDefined(statements.map(s => s.querySummary)); + const name = querySummary || DB_SYSTEM_POSTGRESQL; + + return { + name, + attributes: { + [SENTRY_OP]: DB, + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: DB_SYSTEM_POSTGRESQL, + [DB_QUERY_TEXT]: queryText || undefined, + [DB_QUERY_SUMMARY]: querySummary, + [DB_OPERATION_BATCH_SIZE]: batchSize, + }, + }; +} + +function joinDefined(parts: (string | undefined)[]): string | undefined { + const defined = parts.filter((part): part is string => !!part); + return defined.length ? defined.join('; ') : undefined; +} + +function getQueryText(data: NeonQueryData | undefined): string | undefined { + if (!data || typeof data !== 'object') { + return undefined; + } + if ('query' in data && typeof data.query === 'string') { + return data.query; + } + if (!('strings' in data) || !Array.isArray(data.strings)) { + return undefined; + } + if (typeof data.toParameterizedQuery === 'function') { + try { + // Neon's own rendering also inlines `sql.unsafe()` fragments and nested templates. + return data.toParameterizedQuery().query; + } catch { + // fall through to the plain placeholder rendering + } + } + return data.strings.reduce((text, part, i) => `${text}$${i}${part}`); +} + +/** + * Diagnostics-channel-based `@neondatabase/serverless` integration. + * + * Subscribes to the channels Sentry's code transform injects into the package's bundled pg + * `Client.prototype.query` (WebSocket driver) and into the HTTP executor behind `neon()`, so both + * drivers emit `db` spans with the same shape as the `pg` integration. Requires the Sentry runtime + * hook or bundler plugin to be active. + */ +export const neonIntegration = defineIntegration(_neonIntegration); diff --git a/packages/server-utils/src/orchestrion/channels.ts b/packages/server-utils/src/orchestrion/channels.ts index 4211c8c88d5c..b5919b9eb071 100644 --- a/packages/server-utils/src/orchestrion/channels.ts +++ b/packages/server-utils/src/orchestrion/channels.ts @@ -24,6 +24,7 @@ import { mongodbChannels } from './config/mongodb'; import { mongooseChannels } from './config/mongoose'; import { mysql2Channels } from './config/mysql2'; import { mysqlChannels } from './config/mysql'; +import { neonChannels } from './config/neon'; import { nestjsChannels } from './config/nestjs'; import { openaiChannels } from './config/openai'; import { pgChannels } from './config/pg'; @@ -80,6 +81,7 @@ export const CHANNELS = { ...mongooseChannels, ...mysql2Channels, ...mysqlChannels, + ...neonChannels, ...nestjsChannels, ...openaiChannels, ...pgChannels, diff --git a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts index 683370c0fe61..d3583a3977ec 100644 --- a/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts +++ b/packages/server-utils/src/orchestrion/config/channel-integration-definitions.ts @@ -18,6 +18,7 @@ export const CHANNEL_INTEGRATION_DEFINITIONS = [ { exportName: 'postgresIntegration', modules: ['pg', 'pg-pool'] }, { exportName: 'postgresJsIntegration', modules: ['postgres'] }, + { exportName: 'neonIntegration', modules: ['@neondatabase/serverless'] }, { exportName: 'prismaIntegration', modules: ['@prisma/orm-family-sql'] }, { exportName: 'mysqlIntegration', modules: ['mysql'] }, { exportName: 'mysql2Integration', modules: ['mysql2'] }, diff --git a/packages/server-utils/src/orchestrion/config/index.ts b/packages/server-utils/src/orchestrion/config/index.ts index 2c6a7835db1a..0cf0e363728f 100644 --- a/packages/server-utils/src/orchestrion/config/index.ts +++ b/packages/server-utils/src/orchestrion/config/index.ts @@ -30,6 +30,7 @@ import { mongodbConfig } from './mongodb'; import { mongooseConfig } from './mongoose'; import { mysql2Config } from './mysql2'; import { mysqlConfig } from './mysql'; +import { neonConfig } from './neon'; import { nestjsConfig } from './nestjs'; import { openaiConfig } from './openai'; import { pgConfig } from './pg'; @@ -86,6 +87,7 @@ export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [ ...mongooseConfig, ...mysql2Config, ...mysqlConfig, + ...neonConfig, ...nestjsConfig, ...openaiConfig, ...pgConfig, diff --git a/packages/server-utils/test/integrations/neon.test.ts b/packages/server-utils/test/integrations/neon.test.ts new file mode 100644 index 000000000000..661a65cc7389 --- /dev/null +++ b/packages/server-utils/test/integrations/neon.test.ts @@ -0,0 +1,286 @@ +import { AsyncLocalStorage } from 'node:async_hooks'; +import { tracingChannel } from 'node:diagnostics_channel'; +import { + DB_NAMESPACE, + DB_OPERATION_BATCH_SIZE, + DB_QUERY_SUMMARY, + DB_QUERY_TEXT, + DB_SYSTEM_NAME, + DB_USER, + SENTRY_KIND, + SENTRY_OP, + SENTRY_ORIGIN, + SERVER_ADDRESS, + SERVER_PORT, +} from '@sentry/conventions/attributes'; +import { DB } from '@sentry/conventions/op'; +import type { Client, Scope, Span } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; +import { + _INTERNAL_setSpanForScope, + getDefaultCurrentScope, + getDefaultIsolationScope, + GLOBAL_OBJ, + setAsyncContextStrategy, +} from '@sentry/core'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from 'vitest'; +import { neonIntegration } from '../../src/integrations/neon'; +import { CHANNELS } from '../../src/orchestrion/channels'; + +interface TestStore { + scope: Scope; + isolationScope: Scope; +} + +// Same minimal async-context strategy as the pg tests: `bindTracingChannelToSpan` only binds +// when a `getTracingChannelBinding` exists. +function installTestAsyncContextStrategy(): void { + const asyncStorage = new AsyncLocalStorage(); + + function getScopes(): TestStore { + return asyncStorage.getStore() || { scope: getDefaultCurrentScope(), isolationScope: getDefaultIsolationScope() }; + } + + setAsyncContextStrategy({ + withScope: callback => { + const scope = getScopes().scope.clone(); + const isolationScope = getScopes().isolationScope; + return asyncStorage.run({ scope, isolationScope }, () => callback(scope)); + }, + withSetScope: (scope, callback) => { + const isolationScope = getScopes().isolationScope; + return asyncStorage.run({ scope, isolationScope }, () => callback(scope)); + }, + withIsolationScope: callback => { + const scope = getScopes().scope; + const isolationScope = getScopes().isolationScope.clone(); + return asyncStorage.run({ scope, isolationScope }, () => callback(isolationScope)); + }, + withSetIsolationScope: (isolationScope, callback) => { + const scope = getScopes().scope; + return asyncStorage.run({ scope, isolationScope }, () => callback(isolationScope)); + }, + getCurrentScope: () => getScopes().scope, + getIsolationScope: () => getScopes().isolationScope, + getTracingChannelBinding: () => ({ + asyncLocalStorage: asyncStorage, + getStoreWithActiveSpan: span => { + const scope = getScopes().scope.clone(); + const isolationScope = getScopes().isolationScope; + _INTERNAL_setSpanForScope(scope, span); + return { scope, isolationScope }; + }, + }), + }); +} + +function makeSpan(): Span { + return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn() } as unknown as Span; +} + +interface ChannelContext { + arguments: unknown[]; + self?: unknown; + result?: unknown; +} + +const CONNECTION = { database: 'neondb', host: 'ep-x.aws.neon.tech', port: 5432, user: 'tim' }; + +// Shape of `@neondatabase/serverless`'s `SqlTemplate`, the query data a tagged-template call builds. +function sqlTemplate(strings: string[], values: unknown[]): unknown { + return { + strings, + values, + toParameterizedQuery() { + return { query: strings.reduce((acc, s, i) => `${acc}$${i}${s}`), params: values }; + }, + }; +} + +describe('neonIntegration', () => { + let startInactiveSpanSpy: MockInstance; + let getActiveSpanSpy: MockInstance; + let span: Span; + + beforeAll(() => { + installTestAsyncContextStrategy(); + GLOBAL_OBJ.__SENTRY_ORCHESTRION__ = { runtime: ['@neondatabase/serverless'] }; + neonIntegration().setup?.({ on: () => () => undefined } as unknown as Client); + }); + + afterAll(() => { + setAsyncContextStrategy(undefined); + delete GLOBAL_OBJ.__SENTRY_ORCHESTRION__; + }); + + beforeEach(() => { + span = makeSpan(); + startInactiveSpanSpy = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span); + getActiveSpanSpy = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as Span); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('websocket driver (`Client.prototype.query`)', () => { + it('builds a db span with the pg connection attributes and the neon origin', async () => { + const ctx: ChannelContext = { arguments: ['SELECT * FROM "User"'], self: { connectionParameters: CONNECTION } }; + + await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT * FROM "User"', + attributes: expect.objectContaining({ + [SENTRY_OP]: DB, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: 'postgresql', + [DB_NAMESPACE]: 'neondb', + [DB_USER]: 'tim', + [SERVER_ADDRESS]: 'ep-x.aws.neon.tech', + [SERVER_PORT]: 5432, + [DB_QUERY_TEXT]: 'SELECT * FROM "User"', + [SENTRY_ORIGIN]: 'auto.db.neon', + }), + }), + ); + // The deprecated `db.connection_string` is left to the pg integration. + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.not.objectContaining({ 'db.connection_string': expect.anything() }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('does not start a span without a parent span', async () => { + getActiveSpanSpy.mockReturnValue(undefined); + const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; + + await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); + + expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + }); + }); + + describe('http driver (`neon()`)', () => { + it('builds a db span for `sql.query(text, params)`', async () => { + const ctx: ChannelContext = { + arguments: [{ query: 'SELECT * FROM "User" WHERE id = $1', params: [1] }, undefined], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT "User"', + attributes: expect.objectContaining({ + [SENTRY_OP]: DB, + [SENTRY_KIND]: 'client', + [DB_SYSTEM_NAME]: 'postgresql', + [DB_QUERY_TEXT]: 'SELECT * FROM "User" WHERE id = $1', + [DB_QUERY_SUMMARY]: 'SELECT "User"', + [SENTRY_ORIGIN]: 'auto.db.neon', + [DB_OPERATION_BATCH_SIZE]: undefined, + }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('renders a tagged-template query with positional placeholders', async () => { + const ctx: ChannelContext = { + arguments: [sqlTemplate(['SELECT * FROM "User" WHERE id = ', ' AND name = ', ''], [1, 'tim'])], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'SELECT "User"', + attributes: expect.objectContaining({ + [DB_QUERY_TEXT]: 'SELECT * FROM "User" WHERE id = $1 AND name = $2', + }), + }), + ); + }); + + it('builds one span for a `transaction()` batch with the batch size', async () => { + const ctx: ChannelContext = { + arguments: [ + [ + { query: 'INSERT INTO "User" ("name") VALUES ($1)', params: ['tim'] }, + sqlTemplate(['SELECT * FROM "User"'], []), + ], + [{}, {}], + ], + }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => [[], []], ctx); + + expect(startInactiveSpanSpy).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'INSERT "User"; SELECT "User"', + attributes: expect.objectContaining({ + [DB_QUERY_TEXT]: 'INSERT INTO "User" ("name") VALUES ($1); SELECT * FROM "User"', + [DB_QUERY_SUMMARY]: 'INSERT "User"; SELECT "User"', + [DB_OPERATION_BATCH_SIZE]: 2, + [SENTRY_ORIGIN]: 'auto.db.neon', + }), + }), + ); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('sets error status and ends the span when the request rejects', async () => { + const ctx: ChannelContext = { arguments: [{ query: 'SELECT 1', params: [] }] }; + + await expect( + tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => { + throw new Error('boom'); + }, ctx), + ).rejects.toThrow('boom'); + + expect(span.setStatus).toHaveBeenCalledWith({ code: expect.anything(), message: 'boom' }); + expect(span.end).toHaveBeenCalledTimes(1); + }); + + it('adds the connection attributes once the connection string resolves inside the query', async () => { + const ctx: ChannelContext = { arguments: [{ query: 'SELECT 1', params: [] }] }; + + await tracingChannel(CHANNELS.NEON_HTTP_QUERY).tracePromise(async () => { + // The resolver runs inside the query span's async context. + getActiveSpanSpy.mockReturnValue(span); + await tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).tracePromise( + async () => ({ + resolvedConnectionString: 'postgresql://tim:secret@ep-x.aws.neon.tech/neondb?application_name=x', + resolvedURL: new URL('postgresql://tim:secret@ep-x.aws.neon.tech/neondb?application_name=x'), + }), + { arguments: ['postgresql://tim:secret@ep-x.aws.neon.tech/neondb'] }, + ); + }, ctx); + + // No port in the URL means the Postgres default, like pg's own connection parameters. + expect(span.setAttributes).toHaveBeenCalledWith({ + [DB_SYSTEM_NAME]: 'postgresql', + [DB_NAMESPACE]: 'neondb', + [DB_USER]: 'tim', + [SERVER_ADDRESS]: 'ep-x.aws.neon.tech', + [SERVER_PORT]: 5432, + }); + }); + + it('leaves spans it did not start alone when the connection string resolves', async () => { + const foreignSpan = makeSpan(); + getActiveSpanSpy.mockReturnValue(foreignSpan); + + await tracingChannel(CHANNELS.NEON_RESOLVE_CONNECTION).tracePromise( + async () => ({ resolvedURL: new URL('postgresql://tim:secret@ep-x.aws.neon.tech/neondb') }), + { arguments: [] }, + ); + + expect(foreignSpan.setAttributes).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/server-utils/test/orchestrion/neon-bundle.test.ts b/packages/server-utils/test/orchestrion/neon-bundle.test.ts index e84a1d35ebe9..79d9907467e2 100644 --- a/packages/server-utils/test/orchestrion/neon-bundle.test.ts +++ b/packages/server-utils/test/orchestrion/neon-bundle.test.ts @@ -6,7 +6,9 @@ import { dirname, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { tracingChannel } from 'node:diagnostics_channel'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { neonChannels, neonConfig } from '../../src/orchestrion/config/neon'; +import { SENTRY_INSTRUMENTATIONS } from '../../src/orchestrion/config'; +import { neonChannels } from '../../src/orchestrion/config/neon'; +import { orchestrionTransformOptions } from '../../src/orchestrion/bundler/options'; // The selectors key on internals of Neon's minified bundles, so this runs the bundler plugins' // code transformer over the real published files and executes the result: a release that @@ -63,11 +65,21 @@ describe('neon orchestrion config on the published bundles', () => { afterAll(() => rmSync(outDir, { recursive: true, force: true })); + it('injects the subscriber snippet for the neon integration', () => { + const transformer = createCodeTransformer(orchestrionTransformOptions({})); + const result = transformer.transform( + readFileSync(join(packageDir, 'index.js'), 'utf8'), + join(packageDir, 'index.js'), + ); + + expect(result?.code).toContain('orchestrionModuleInjected("@neondatabase/serverless", () => neonIntegration())'); + }); + it.each([ ['index.js', 'cjs'], ['index.mjs', 'esm'], ])('instruments both drivers in %s', async (file, format) => { - const transformer = createCodeTransformer({ instrumentations: neonConfig }); + const transformer = createCodeTransformer({ instrumentations: SENTRY_INSTRUMENTATIONS }); const source = join(packageDir, file); const result = transformer.transform(readFileSync(source, 'utf8'), source); expect(result).not.toBeNull(); diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index 8c8576aeb7ae..fc4f29d21181 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -82,6 +82,7 @@ export { parameterize, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, redisIntegration, requestDataIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index cb0730b4c513..d01ea0d03fc2 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -81,6 +81,7 @@ export { parameterize, postgresIntegration, postgresJsIntegration, + neonIntegration, prismaIntegration, redisIntegration, requestDataIntegration,