diff --git a/packages/node/src/integrations/tracing/firebase/firebase.ts b/packages/node/src/integrations/tracing/firebase/firebase.ts index 13eb65c42568..d51d0ea039c2 100644 --- a/packages/node/src/integrations/tracing/firebase/firebase.ts +++ b/packages/node/src/integrations/tracing/firebase/firebase.ts @@ -1,37 +1,11 @@ import type { IntegrationFn } from '@sentry/core'; -import { captureException, defineIntegration, flush, SEMANTIC_ATTRIBUTE_SENTRY_OP } from '@sentry/core'; -import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; -import { FirebaseInstrumentation, type FirebaseInstrumentationConfig } from './otel'; +import { defineIntegration } from '@sentry/core'; +import { generateInstrumentOnce } from '@sentry/node-core'; +import { FirebaseInstrumentation } from './otel'; const INTEGRATION_NAME = 'Firebase'; -const config: FirebaseInstrumentationConfig = { - firestoreSpanCreationHook: span => { - addOriginToSpan(span, 'auto.firebase.otel.firestore'); - - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'db.query'); - }, - functions: { - requestHook: span => { - addOriginToSpan(span, 'auto.firebase.otel.functions'); - - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.request'); - }, - errorHook: async (_, error) => { - if (error) { - captureException(error, { - mechanism: { - type: 'auto.firebase.otel.functions', - handled: false, - }, - }); - await flush(2000); - } - }, - }, -}; - -export const instrumentFirebase = generateInstrumentOnce(INTEGRATION_NAME, () => new FirebaseInstrumentation(config)); +export const instrumentFirebase = generateInstrumentOnce(INTEGRATION_NAME, () => new FirebaseInstrumentation()); const _firebaseIntegration = (() => { return { diff --git a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts index bd55dc708315..ed57b02b1035 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/firebaseInstrumentation.ts @@ -1,29 +1,20 @@ -import { InstrumentationBase, type InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; +import type { InstrumentationConfig, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; +import { InstrumentationBase } from '@opentelemetry/instrumentation'; import { SDK_VERSION } from '@sentry/core'; import { patchFirestore } from './patches/firestore'; import { patchFunctions } from './patches/functions'; -import type { FirebaseInstrumentationConfig } from './types'; -const DefaultFirebaseInstrumentationConfig: FirebaseInstrumentationConfig = {}; const firestoreSupportedVersions = ['>=3.0.0 <5']; // firebase 9+ const functionsSupportedVersions = ['>=6.0.0 <7']; // firebase-functions v2 /** * Instrumentation for Firebase services, specifically Firestore. */ -export class FirebaseInstrumentation extends InstrumentationBase { - public constructor(config: FirebaseInstrumentationConfig = DefaultFirebaseInstrumentationConfig) { +export class FirebaseInstrumentation extends InstrumentationBase { + public constructor(config: InstrumentationConfig = {}) { super('@sentry/instrumentation-firebase', SDK_VERSION, config); } - /** - * sets config - * @param config - */ - public override setConfig(config: FirebaseInstrumentationConfig = {}): void { - super.setConfig({ ...DefaultFirebaseInstrumentationConfig, ...config }); - } - /** * * @protected @@ -32,8 +23,8 @@ export class FirebaseInstrumentation extends InstrumentationBase void; /** * - * @param tracer - Opentelemetry Tracer * @param firestoreSupportedVersions - supported version of firebase/firestore * @param wrap - reference to native instrumentation wrap function * @param unwrap - reference to native instrumentation wrap function */ export function patchFirestore( - tracer: Tracer, firestoreSupportedVersions: string[], wrap: ShimmerWrap, unwrap: ShimmerUnwrap, - config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { - const defaultFirestoreSpanCreationHook: FirestoreSpanCreationHook = () => {}; - - let firestoreSpanCreationHook: FirestoreSpanCreationHook = defaultFirestoreSpanCreationHook; - const configFirestoreSpanCreationHook = config.firestoreSpanCreationHook; - - if (typeof configFirestoreSpanCreationHook === 'function') { - firestoreSpanCreationHook = (span: Span) => { - safeExecuteInTheMiddle( - () => configFirestoreSpanCreationHook(span), - error => { - if (!error) { - return; - } - diag.error(error?.message); - }, - true, - ); - }; - } - const moduleFirestoreCJS = new InstrumentationNodeModuleDefinition( '@firebase/firestore', firestoreSupportedVersions, // eslint-disable-next-line @typescript-eslint/no-explicit-any - (moduleExports: any) => wrapMethods(moduleExports, wrap, unwrap, tracer, firestoreSpanCreationHook), + (moduleExports: any) => wrapMethods(moduleExports, wrap, unwrap), ); const files: string[] = [ '@firebase/firestore/dist/lite/index.node.cjs.js', @@ -91,7 +65,7 @@ export function patchFirestore( new InstrumentationNodeModuleFile( file, firestoreSupportedVersions, - moduleExports => wrapMethods(moduleExports, wrap, unwrap, tracer, firestoreSpanCreationHook), + moduleExports => wrapMethods(moduleExports, wrap, unwrap), moduleExports => unwrapMethods(moduleExports, unwrap), ), ); @@ -105,16 +79,14 @@ function wrapMethods( moduleExports: any, wrap: ShimmerWrap, unwrap: ShimmerUnwrap, - tracer: Tracer, - firestoreSpanCreationHook: FirestoreSpanCreationHook, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): any { unwrapMethods(moduleExports, unwrap); - wrap(moduleExports, 'addDoc', patchAddDoc(tracer, firestoreSpanCreationHook)); - wrap(moduleExports, 'getDocs', patchGetDocs(tracer, firestoreSpanCreationHook)); - wrap(moduleExports, 'setDoc', patchSetDoc(tracer, firestoreSpanCreationHook)); - wrap(moduleExports, 'deleteDoc', patchDeleteDoc(tracer, firestoreSpanCreationHook)); + wrap(moduleExports, 'addDoc', patchAddDoc()); + wrap(moduleExports, 'getDocs', patchGetDocs()); + wrap(moduleExports, 'setDoc', patchSetDoc()); + wrap(moduleExports, 'deleteDoc', patchDeleteDoc()); return moduleExports; } @@ -134,10 +106,7 @@ function unwrapMethods( return moduleExports; } -function patchAddDoc( - tracer: Tracer, - firestoreSpanCreationHook: FirestoreSpanCreationHook, -): ( +function patchAddDoc(): ( original: AddDocType, ) => ( this: FirebaseInstrumentation, @@ -149,36 +118,22 @@ function patchAddDoc( reference: CollectionReference, data: WithFieldValue, ): Promise> { - const span = startDBSpan(tracer, 'addDoc', reference); - firestoreSpanCreationHook(span); - return executeContextWithSpan>>(span, () => { - return original(reference, data); - }); + return startFirestoreSpan('addDoc', reference, () => original(reference, data)); }; }; } -function patchDeleteDoc( - tracer: Tracer, - firestoreSpanCreationHook: FirestoreSpanCreationHook, -): ( +function patchDeleteDoc(): ( original: DeleteDocType, ) => (this: FirebaseInstrumentation, reference: DocumentReference) => Promise { return function deleteDoc(original: DeleteDocType) { return function (reference: DocumentReference): Promise { - const span = startDBSpan(tracer, 'deleteDoc', reference.parent || reference); - firestoreSpanCreationHook(span); - return executeContextWithSpan>(span, () => { - return original(reference); - }); + return startFirestoreSpan('deleteDoc', reference.parent || reference, () => original(reference)); }; }; } -function patchGetDocs( - tracer: Tracer, - firestoreSpanCreationHook: FirestoreSpanCreationHook, -): ( +function patchGetDocs(): ( original: GetDocsType, ) => ( this: FirebaseInstrumentation, @@ -188,19 +143,12 @@ function patchGetDocs( return function ( reference: CollectionReference, ): Promise> { - const span = startDBSpan(tracer, 'getDocs', reference); - firestoreSpanCreationHook(span); - return executeContextWithSpan>>(span, () => { - return original(reference); - }); + return startFirestoreSpan('getDocs', reference, () => original(reference)); }; }; } -function patchSetDoc( - tracer: Tracer, - firestoreSpanCreationHook: FirestoreSpanCreationHook, -): ( +function patchSetDoc(): ( original: SetDocType, ) => ( this: FirebaseInstrumentation, @@ -214,48 +162,36 @@ function patchSetDoc( data: WithFieldValue & PartialWithFieldValue, options?: SetOptions, ): Promise { - const span = startDBSpan(tracer, 'setDoc', reference.parent || reference); - firestoreSpanCreationHook(span); - - return executeContextWithSpan>(span, () => { + return startFirestoreSpan('setDoc', reference.parent || reference, () => { return typeof options !== 'undefined' ? original(reference, data, options) : original(reference, data); }); }; }; } -function executeContextWithSpan(span: Span, callback: () => T): T { - return context.with(trace.setSpan(context.active(), span), () => { - return safeExecuteInTheMiddle( - (): T => { - return callback(); - }, - err => { - if (err) { - span.recordException(err); - } - span.end(); - }, - true, - ); - }); -} - -function startDBSpan( - tracer: Tracer, +function startFirestoreSpan( spanName: string, reference: CollectionReference | DocumentReference, -): Span { - const span = tracer.startSpan(`${spanName} ${reference.path}`, { kind: SpanKind.CLIENT }); - addAttributes(span, reference); - span.setAttribute(ATTR_DB_OPERATION_NAME, spanName); - return span; + callback: () => T, +): T { + return startSpan( + { + name: `${spanName} ${reference.path}`, + op: 'db.query', + kind: SPAN_KIND.CLIENT, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.otel.firestore', + [ATTR_DB_OPERATION_NAME]: spanName, + ...buildAttributes(reference), + }, + }, + callback, + ); } /** * Gets the server address and port attributes from the Firestore settings. * It's best effort to extract the address and port from the settings, especially for IPv6. - * @param span - The span to set attributes on. * @param settings - The Firestore settings containing host information. */ export function getPortAndAddress(settings: FirestoreSettings): { @@ -303,10 +239,9 @@ export function getPortAndAddress(settings: FirestoreSettings): { }; } -function addAttributes( - span: Span, +function buildAttributes( reference: CollectionReference | DocumentReference, -): void { +): SpanAttributes { const firestoreApp: FirebaseApp = reference.firestore.app; const firestoreOptions: FirebaseOptions = firestoreApp.options; const json: { settings?: FirestoreSettings } = reference.firestore.toJSON() || {}; @@ -332,5 +267,5 @@ function addAttributes( attributes[ATTR_SERVER_PORT] = port; } - span.setAttributes(attributes); + return attributes; } diff --git a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts index 76c6e796df7c..829f465560c9 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/patches/functions.ts @@ -1,69 +1,30 @@ -import type { Span, Tracer } from '@opentelemetry/api'; -import { context, diag, SpanKind, trace } from '@opentelemetry/api'; import type { InstrumentationBase } from '@opentelemetry/instrumentation'; -import { InstrumentationNodeModuleDefinition, isWrapped, safeExecuteInTheMiddle } from '@opentelemetry/instrumentation'; +import { InstrumentationNodeModuleDefinition, isWrapped } from '@opentelemetry/instrumentation'; import { InstrumentationNodeModuleFile } from '../../../InstrumentationNodeModuleFile'; import type { SpanAttributes } from '@sentry/core'; +import { + captureException, + flush, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SPAN_KIND, + SPAN_STATUS_ERROR, + startInactiveSpan, + withActiveSpan, +} from '@sentry/core'; import type { FirebaseInstrumentation } from '../firebaseInstrumentation'; -import type { - AvailableFirebaseFunctions, - FirebaseFunctions, - FirebaseInstrumentationConfig, - OverloadedParameters, - RequestHook, - ResponseHook, -} from '../types'; +import type { AvailableFirebaseFunctions, FirebaseFunctions, OverloadedParameters } from '../types'; /** * Patches Firebase Functions v2 to add OpenTelemetry instrumentation - * @param tracer - Opentelemetry Tracer * @param functionsSupportedVersions - supported versions of firebase-functions * @param wrap - reference to native instrumentation wrap function * @param unwrap - reference to native instrumentation unwrap function - * @param config - Firebase instrumentation config */ export function patchFunctions( - tracer: Tracer, functionsSupportedVersions: string[], wrap: InstrumentationBase['_wrap'], unwrap: InstrumentationBase['_unwrap'], - config: FirebaseInstrumentationConfig, ): InstrumentationNodeModuleDefinition { - let requestHook: RequestHook = () => {}; - let responseHook: ResponseHook = () => {}; - const errorHook = config.functions?.errorHook; - const configRequestHook = config.functions?.requestHook; - const configResponseHook = config.functions?.responseHook; - - if (typeof configResponseHook === 'function') { - responseHook = (span: Span, err: unknown) => { - safeExecuteInTheMiddle( - () => configResponseHook(span, err), - error => { - if (!error) { - return; - } - diag.error(error?.message); - }, - true, - ); - }; - } - if (typeof configRequestHook === 'function') { - requestHook = (span: Span) => { - safeExecuteInTheMiddle( - () => configRequestHook(span), - error => { - if (!error) { - return; - } - diag.error(error?.message); - }, - true, - ); - }; - } - const moduleFunctionsCJS = new InstrumentationNodeModuleDefinition('firebase-functions', functionsSupportedVersions); const modulesToInstrument = [ { name: 'firebase-functions/lib/v2/providers/https.js', triggerType: 'function' }, @@ -77,15 +38,7 @@ export function patchFunctions( new InstrumentationNodeModuleFile( name, functionsSupportedVersions, - moduleExports => - wrapCommonFunctions( - moduleExports, - wrap, - unwrap, - tracer, - { requestHook, responseHook, errorHook }, - triggerType, - ), + moduleExports => wrapCommonFunctions(moduleExports, wrap, unwrap, triggerType), moduleExports => unwrapCommonFunctions(moduleExports, unwrap), ), ); @@ -97,14 +50,10 @@ export function patchFunctions( /** * Patches Cloud Functions for Firebase (v2) to add OpenTelemetry instrumentation * - * @param tracer - Opentelemetry Tracer - * @param functionsConfig - Firebase instrumentation config * @param triggerType - Type of trigger * @returns A function that patches the function */ export function patchV2Functions( - tracer: Tracer, - functionsConfig: FirebaseInstrumentationConfig['functions'], triggerType: string, ): (original: T) => (...args: OverloadedParameters) => ReturnType { return function v2FunctionsWrapper(original: T): (...args: OverloadedParameters) => ReturnType { @@ -118,11 +67,9 @@ export function patchV2Functions { const functionName = process.env.FUNCTION_TARGET || process.env.K_SERVICE || 'unknown'; - const span = tracer.startSpan(`firebase.function.${triggerType}`, { - kind: SpanKind.SERVER, - }); const attributes: SpanAttributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.firebase.otel.functions', 'faas.name': functionName, 'faas.trigger': triggerType, 'faas.provider': 'firebase', @@ -136,35 +83,31 @@ export function patchV2Functions { - let error: Error | undefined; - let result: T | undefined; + // Use an inactive span (not `startSpan`) so we can end the span before flushing on error. + const span = startInactiveSpan({ + name: `firebase.function.${triggerType}`, + op: 'http.request', + kind: SPAN_KIND.SERVER, + attributes, + }); + return withActiveSpan(span, async () => { try { - result = await handler.apply(this, handlerArgs); - } catch (e) { - error = e as Error; - } - - functionsConfig?.responseHook?.(span, error); - - if (error) { - span.recordException(error); - } - - span.end(); - - if (error) { - await functionsConfig?.errorHook?.(span, error); + const result = await handler.apply(this, handlerArgs); + span.end(); + return result; + } catch (error) { + span.setStatus({ code: SPAN_STATUS_ERROR }); + captureException(error, { + mechanism: { + type: 'auto.firebase.otel.functions', + handled: false, + }, + }); + span.end(); + await flush(2000); throw error; } - - return result; }); }; @@ -179,62 +122,38 @@ export function patchV2Functions['_wrap'], - unwrap: InstrumentationBase['_unwrap'], - tracer: Tracer, - functionsConfig: FirebaseInstrumentationConfig['functions'], + wrap: InstrumentationBase['_wrap'], + unwrap: InstrumentationBase['_unwrap'], triggerType: 'function' | 'firestore' | 'scheduler' | 'storage', ): AvailableFirebaseFunctions { unwrapCommonFunctions(moduleExports, unwrap); switch (triggerType) { case 'function': - wrap(moduleExports, 'onRequest', patchV2Functions(tracer, functionsConfig, 'http.request')); - wrap(moduleExports, 'onCall', patchV2Functions(tracer, functionsConfig, 'http.call')); + wrap(moduleExports, 'onRequest', patchV2Functions('http.request')); + wrap(moduleExports, 'onCall', patchV2Functions('http.call')); break; case 'firestore': - wrap(moduleExports, 'onDocumentCreated', patchV2Functions(tracer, functionsConfig, 'firestore.document.created')); - wrap(moduleExports, 'onDocumentUpdated', patchV2Functions(tracer, functionsConfig, 'firestore.document.updated')); - wrap(moduleExports, 'onDocumentDeleted', patchV2Functions(tracer, functionsConfig, 'firestore.document.deleted')); - wrap(moduleExports, 'onDocumentWritten', patchV2Functions(tracer, functionsConfig, 'firestore.document.written')); - wrap( - moduleExports, - 'onDocumentCreatedWithAuthContext', - patchV2Functions(tracer, functionsConfig, 'firestore.document.created'), - ); - wrap( - moduleExports, - 'onDocumentUpdatedWithAuthContext', - patchV2Functions(tracer, functionsConfig, 'firestore.document.updated'), - ); - - wrap( - moduleExports, - 'onDocumentDeletedWithAuthContext', - patchV2Functions(tracer, functionsConfig, 'firestore.document.deleted'), - ); - - wrap( - moduleExports, - 'onDocumentWrittenWithAuthContext', - patchV2Functions(tracer, functionsConfig, 'firestore.document.written'), - ); + wrap(moduleExports, 'onDocumentCreated', patchV2Functions('firestore.document.created')); + wrap(moduleExports, 'onDocumentUpdated', patchV2Functions('firestore.document.updated')); + wrap(moduleExports, 'onDocumentDeleted', patchV2Functions('firestore.document.deleted')); + wrap(moduleExports, 'onDocumentWritten', patchV2Functions('firestore.document.written')); + wrap(moduleExports, 'onDocumentCreatedWithAuthContext', patchV2Functions('firestore.document.created')); + wrap(moduleExports, 'onDocumentUpdatedWithAuthContext', patchV2Functions('firestore.document.updated')); + wrap(moduleExports, 'onDocumentDeletedWithAuthContext', patchV2Functions('firestore.document.deleted')); + wrap(moduleExports, 'onDocumentWrittenWithAuthContext', patchV2Functions('firestore.document.written')); break; case 'scheduler': - wrap(moduleExports, 'onSchedule', patchV2Functions(tracer, functionsConfig, 'scheduler.scheduled')); + wrap(moduleExports, 'onSchedule', patchV2Functions('scheduler.scheduled')); break; case 'storage': - wrap(moduleExports, 'onObjectFinalized', patchV2Functions(tracer, functionsConfig, 'storage.object.finalized')); - wrap(moduleExports, 'onObjectArchived', patchV2Functions(tracer, functionsConfig, 'storage.object.archived')); - wrap(moduleExports, 'onObjectDeleted', patchV2Functions(tracer, functionsConfig, 'storage.object.deleted')); - wrap( - moduleExports, - 'onObjectMetadataUpdated', - patchV2Functions(tracer, functionsConfig, 'storage.object.metadataUpdated'), - ); + wrap(moduleExports, 'onObjectFinalized', patchV2Functions('storage.object.finalized')); + wrap(moduleExports, 'onObjectArchived', patchV2Functions('storage.object.archived')); + wrap(moduleExports, 'onObjectDeleted', patchV2Functions('storage.object.deleted')); + wrap(moduleExports, 'onObjectMetadataUpdated', patchV2Functions('storage.object.metadataUpdated')); break; } @@ -243,7 +162,7 @@ function wrapCommonFunctions( function unwrapCommonFunctions( moduleExports: AvailableFirebaseFunctions, - unwrap: InstrumentationBase['_unwrap'], + unwrap: InstrumentationBase['_unwrap'], ): AvailableFirebaseFunctions { const methods: (keyof AvailableFirebaseFunctions)[] = [ 'onSchedule', diff --git a/packages/node/src/integrations/tracing/firebase/otel/types.ts b/packages/node/src/integrations/tracing/firebase/otel/types.ts index 4b94cbefe02c..6bba7d59a35b 100644 --- a/packages/node/src/integrations/tracing/firebase/otel/types.ts +++ b/packages/node/src/integrations/tracing/firebase/otel/types.ts @@ -1,5 +1,3 @@ -import type { Span } from '@opentelemetry/api'; -import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; /* eslint-disable @typescript-eslint/no-explicit-any */ // Inlined types from 'firebase/app' @@ -84,28 +82,6 @@ export interface FirestoreSettings { useFetchStreams?: boolean; } -/** - * Firebase Auto Instrumentation - */ -export interface FirebaseInstrumentationConfig extends InstrumentationConfig { - firestoreSpanCreationHook?: FirestoreSpanCreationHook; - functions?: FunctionsConfig; -} - -export interface FunctionsConfig { - requestHook?: RequestHook; - responseHook?: ResponseHook; - errorHook?: ErrorHook; -} - -export type RequestHook = (span: Span) => void; -export type ResponseHook = (span: Span, error?: unknown) => void; -export type ErrorHook = (span: Span, error?: unknown) => Promise | void; - -export interface FirestoreSpanCreationHook { - (span: Span): void; -} - // Function types (addDoc, getDocs, setDoc, deleteDoc) are defined below as types export type GetDocsType = ( query: CollectionReference,