diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore b/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore index 48b1bd712db4..715e7980bcde 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore +++ b/dev-packages/e2e-tests/test-applications/node-firebase/.gitignore @@ -41,6 +41,7 @@ lerna-debug.log* .env.test.local .env.production.local .env.local +!functions/.env.local # temp directory .temp diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts index 4eff7dbe5e26..d2a4e05c3fe4 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/firestore-app/src/init.ts @@ -4,7 +4,6 @@ import * as Sentry from '@sentry/node'; // default. This file is imported before `app.ts` imports `firebase/firestore/lite`, so the // channel-injection hooks are installed before firestore loads. Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/.env.local b/dev-packages/e2e-tests/test-applications/node-firebase/functions/.env.local new file mode 100644 index 000000000000..9d18096edef7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/.env.local @@ -0,0 +1,2 @@ +# The emulator loads firebase-functions before the handler module calls Sentry.init(). +NODE_OPTIONS=--import=@sentry/node/import diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts index 60959f8e932b..c3b4a642375a 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/functions/src/init.ts @@ -1,7 +1,6 @@ import * as Sentry from '@sentry/node'; Sentry.init({ - traceLifecycle: 'static', dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', tracesSampleRate: 1.0, diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts index a250d80bdb31..1a1093a43031 100644 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/functions.test.ts @@ -1,154 +1,105 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; +import { + collectStreamedSpansUntilSegment, + getSpanOp, + waitForError, + waitForStreamedSpan, +} from '@sentry-internal/test-utils'; -// FIXME: firebase-functions runs inside the Firebase emulator, where the channel-injection runtime -// module hook doesn't transform the emulator-loaded handlers, so no channel spans are produced. -// (Firestore in a plain Node process works.) Deferred; tracked separately for a channel firebase- -// functions emulator fix. -test.fixme('should only call the function once without any extra calls', async () => { - const serverTransactionPromise = waitForTransaction('node-firebase', span => { - return span.transaction === 'firebase.function.http.request'; - }); - - await fetch(`http://localhost:5001/demo-functions/default/helloWorld`); - - const transactionEvent = await serverTransactionPromise; - - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); - expect(transactionEvent.contexts).toEqual( - expect.objectContaining({ - trace: expect.objectContaining({ - data: { - 'cloud.project_id': 'demo-functions', - 'faas.name': 'helloWorld', - 'faas.provider': 'firebase', - 'faas.trigger': 'http.request', - 'sentry.kind': 'server', - 'sentry.op': 'http.request', - 'sentry.origin': 'auto.firebase.functions', - 'sentry.sample_rate': expect.any(Number), - 'sentry.segment.name.source': 'route', - }, - op: 'http.request', - origin: 'auto.firebase.functions', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), - }), - }), +test('should create one segment for an HTTP function', async () => { + const spansPromise = collectStreamedSpansUntilSegment( + 'node-firebase', + span => span.name === 'firebase.function.http.request' && span.attributes['faas.name']?.value === 'helloWorld', ); -}); - -test.fixme('should send failed transaction when the function fails', async () => { - const errorEventPromise = waitForError('node-firebase', () => true); - const serverTransactionPromise = waitForTransaction('node-firebase', span => { - return !!span.transaction; - }); - - await fetch(`http://localhost:5001/demo-functions/default/unhandeledError`); - const transactionEvent = await serverTransactionPromise; - const errorEvent = await errorEventPromise; + const response = await fetch('http://localhost:5001/demo-functions/default/helloWorld'); - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); - expect(transactionEvent.contexts?.trace?.trace_id).toEqual(errorEvent.contexts?.trace?.trace_id); - expect(errorEvent).toMatchObject({ - exception: { - values: [ - { - type: 'Error', - value: 'There is an error!', - mechanism: { - type: 'auto.firebase.functions', - handled: false, - }, - }, - ], - }, + expect(response.ok).toBe(true); + const spans = await spansPromise; + expect(spans).toHaveLength(1); + const span = spans[0]!; + expect(getSpanOp(span)).toBe('function.gcp'); + expect(span).toMatchObject({ + name: 'firebase.function.http.request', + status: 'ok', + span_id: expect.any(String), + trace_id: expect.any(String), + attributes: expect.objectContaining({ + 'cloud.project_id': { value: 'demo-functions', type: 'string' }, + 'faas.name': { value: 'helloWorld', type: 'string' }, + 'faas.provider': { value: 'firebase', type: 'string' }, + 'faas.trigger': { value: 'http.request', type: 'string' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'sentry.origin': { value: 'auto.firebase.functions', type: 'string' }, + 'sentry.sample_rate': { value: expect.any(Number), type: 'integer' }, + 'sentry.segment.name.source': { value: 'component', type: 'string' }, + }), }); }); -test.fixme('should create a document and trigger onDocumentCreated and another with authContext', async () => { - const serverTransactionPromise = waitForTransaction('node-firebase', span => { - return span.transaction === 'firebase.function.http.request'; - }); +test('should send failed span when the function fails', async () => { + const errorPromise = waitForError( + 'node-firebase', + event => event.exception?.values?.[0]?.value === 'There is an error!', + ); + const spanPromise = waitForStreamedSpan( + 'node-firebase', + span => + span.is_segment && + span.name === 'firebase.function.http.request' && + span.attributes['faas.name']?.value === 'unhandeledError', + ); - const serverTransactionOnDocumentCreatePromise = waitForTransaction('node-firebase', span => { - return ( - span.transaction === 'firebase.function.firestore.document.created' && - span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreate' - ); - }); + await fetch('http://localhost:5001/demo-functions/default/unhandeledError'); - const serverTransactionOnDocumentWithAuthContextCreatePromise = waitForTransaction('node-firebase', span => { - return ( - span.transaction === 'firebase.function.firestore.document.created' && - span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreateWithAuthContext' - ); - }); + const span = await spanPromise; + const error = await errorPromise; + expect(span.status).toBe('error'); + expect(span.trace_id).toBe(error.contexts?.trace?.trace_id); + expect(span.span_id).toBe(error.contexts?.trace?.span_id); + expect(error.exception?.values).toEqual([ + expect.objectContaining({ + type: 'Error', + value: 'There is an error!', + mechanism: { type: 'auto.firebase.functions', handled: false }, + }), + ]); +}); - await fetch(`http://localhost:5001/demo-functions/default/onCallSomething`); +test('should create a document and trigger onDocumentCreated and another with authContext', async () => { + const functions = [ + { name: 'onCallSomething', trigger: 'http.request' }, + { name: 'onDocumentCreate', trigger: 'firestore.document.created' }, + { name: 'onDocumentCreateWithAuthContext', trigger: 'firestore.document.created' }, + ]; + const spanPromises = functions.map(({ name }) => + collectStreamedSpansUntilSegment('node-firebase', span => span.attributes['faas.name']?.value === name), + ); - const transactionEvent = await serverTransactionPromise; - const transactionEventOnDocumentCreate = await serverTransactionOnDocumentCreatePromise; - const transactionEventOnDocumentWithAuthContextCreate = await serverTransactionOnDocumentWithAuthContextCreatePromise; + const response = await fetch('http://localhost:5001/demo-functions/default/onCallSomething'); - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); - expect(transactionEvent.contexts?.trace).toEqual({ - data: { - 'cloud.project_id': 'demo-functions', - 'faas.name': 'onCallSomething', - 'faas.provider': 'firebase', - 'faas.trigger': 'http.request', - 'sentry.kind': 'server', - 'sentry.op': 'http.request', - 'sentry.origin': 'auto.firebase.functions', - 'sentry.sample_rate': expect.any(Number), - 'sentry.segment.name.source': 'route', - }, - op: 'http.request', - origin: 'auto.firebase.functions', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), - }); - expect(transactionEvent.spans).toHaveLength(3); - expect(transactionEventOnDocumentCreate.contexts?.trace).toEqual({ - data: { - 'cloud.project_id': 'demo-functions', - 'faas.name': 'onDocumentCreate', - 'faas.provider': 'firebase', - 'faas.trigger': 'firestore.document.created', - 'sentry.kind': 'server', - 'sentry.op': expect.any(String), - 'sentry.origin': 'auto.firebase.functions', - 'sentry.sample_rate': expect.any(Number), - 'sentry.segment.name.source': 'route', - }, - op: expect.any(String), - origin: 'auto.firebase.functions', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), - }); - expect(transactionEventOnDocumentCreate.spans).toHaveLength(2); - expect(transactionEventOnDocumentWithAuthContextCreate.contexts?.trace).toEqual({ - data: { - 'cloud.project_id': 'demo-functions', - 'faas.name': 'onDocumentCreateWithAuthContext', - 'faas.provider': 'firebase', - 'faas.trigger': 'firestore.document.created', - 'sentry.kind': 'server', - 'sentry.op': expect.any(String), - 'sentry.origin': 'auto.firebase.functions', - 'sentry.sample_rate': expect.any(Number), - 'sentry.segment.name.source': 'route', - }, - op: expect.any(String), - origin: 'auto.firebase.functions', - span_id: expect.any(String), - status: 'ok', - trace_id: expect.any(String), + expect(response.ok).toBe(true); + const traces = await Promise.all(spanPromises); + functions.forEach(({ name, trigger }, index) => { + const spans = traces[index]!; + expect(spans).toHaveLength(1); + const segment = spans[0]!; + expect(segment).toMatchObject({ + name: `firebase.function.${trigger}`, + status: 'ok', + span_id: expect.any(String), + trace_id: expect.any(String), + attributes: expect.objectContaining({ + 'cloud.project_id': { value: 'demo-functions', type: 'string' }, + 'faas.name': { value: name, type: 'string' }, + 'faas.provider': { value: 'firebase', type: 'string' }, + 'faas.trigger': { value: trigger, type: 'string' }, + 'sentry.kind': { value: 'server', type: 'string' }, + 'sentry.op': { value: 'function.gcp', type: 'string' }, + 'sentry.origin': { value: 'auto.firebase.functions', type: 'string' }, + 'sentry.sample_rate': { value: expect.any(Number), type: 'integer' }, + 'sentry.segment.name.source': { value: 'component', type: 'string' }, + }), + }); }); - expect(transactionEventOnDocumentWithAuthContextCreate.spans).toHaveLength(0); }); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/spans.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/spans.test.ts new file mode 100644 index 000000000000..dd4b11aa7586 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-firebase/tests/spans.test.ts @@ -0,0 +1,51 @@ +import { expect, test } from '@playwright/test'; +import { collectStreamedSpansUntilSegment } from '@sentry-internal/test-utils'; + +// The orchestrion spans are Sentry-native, so they carry no span-kind attribute (`sentry.kind`). +const origin = 'auto.firebase.firestore'; + +function firestoreSpan(operation: string): unknown { + return expect.objectContaining({ + name: `${operation} cities`, + is_segment: false, + parent_span_id: expect.any(String), + trace_id: expect.any(String), + span_id: expect.any(String), + end_timestamp: expect.any(Number), + start_timestamp: expect.any(Number), + status: 'ok', + attributes: expect.objectContaining({ + 'db.collection.name': { value: 'cities', type: 'string' }, + 'db.namespace': { value: '[DEFAULT]', type: 'string' }, + 'db.operation.name': { value: operation, type: 'string' }, + 'db.system.name': { value: 'firebase.firestore', type: 'string' }, + 'firebase.firestore.options.projectId': { value: 'sentry-15d85', type: 'string' }, + 'firebase.firestore.type': { value: 'collection', type: 'string' }, + 'server.address': { value: '127.0.0.1', type: 'string' }, + 'server.port': { value: 8080, type: 'integer' }, + 'sentry.origin': { value: origin, type: 'string' }, + 'sentry.op': { value: 'db.query', type: 'string' }, + }), + }); +} + +const spanAddDoc = firestoreSpan('addDoc'); +const spanSetDocs = firestoreSpan('setDoc'); +const spanGetDocs = firestoreSpan('getDocs'); +const spanDeleteDoc = firestoreSpan('deleteDoc'); + +test('should add, set, get and delete document', async ({ baseURL }) => { + const serverSegmentPromise = collectStreamedSpansUntilSegment('node-firebase', 'Test Transaction'); + + await fetch(`${baseURL}/test`); + + const segmentEventSpans = await serverSegmentPromise; + const segmentEvent = segmentEventSpans.find(segment => segment.is_segment && segment.name === 'Test Transaction')!; + + expect(segmentEvent.name).toEqual('Test Transaction'); + const children = segmentEventSpans.filter( + span => !span.is_segment && span.attributes['sentry.segment.id']?.value === segmentEvent.span_id, + ); + expect(children).toHaveLength(4); + expect(children).toEqual(expect.arrayContaining([spanAddDoc, spanSetDocs, spanGetDocs, spanDeleteDoc])); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts deleted file mode 100644 index 00a71bb47396..000000000000 --- a/dev-packages/e2e-tests/test-applications/node-firebase/tests/transactions.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; - -// The orchestrion spans are Sentry-native, so they carry no span-kind attribute (`sentry.kind`). -const origin = 'auto.firebase.firestore'; - -function firestoreSpan(operation: string): unknown { - const data: Record = { - 'db.collection.name': 'cities', - 'db.namespace': '[DEFAULT]', - 'db.operation.name': operation, - 'db.system.name': 'firebase.firestore', - 'firebase.firestore.options.projectId': 'sentry-15d85', - 'firebase.firestore.type': 'collection', - 'server.address': '127.0.0.1', - 'server.port': 8080, - 'sentry.origin': origin, - 'sentry.op': 'db.query', - }; - - return expect.objectContaining({ - description: `${operation} cities`, - data: expect.objectContaining(data), - op: 'db.query', - origin, - parent_span_id: expect.any(String), - trace_id: expect.any(String), - span_id: expect.any(String), - timestamp: expect.any(Number), - start_timestamp: expect.any(Number), - status: 'ok', - }); -} - -const spanAddDoc = firestoreSpan('addDoc'); -const spanSetDocs = firestoreSpan('setDoc'); -const spanGetDocs = firestoreSpan('getDocs'); -const spanDeleteDoc = firestoreSpan('deleteDoc'); - -test('should add, set, get and delete document', async ({ baseURL, page }) => { - const serverTransactionPromise = waitForTransaction('node-firebase', span => { - return span.transaction === 'Test Transaction'; - }); - - await fetch(`${baseURL}/test`); - - const transactionEvent = await serverTransactionPromise; - - expect(transactionEvent.transaction).toEqual('Test Transaction'); - expect(transactionEvent.spans?.length).toEqual(4); - - expect(transactionEvent.spans).toEqual(expect.arrayContaining([spanAddDoc, spanSetDocs, spanGetDocs, spanDeleteDoc])); -}); diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-electron/index.electron.js b/dev-packages/e2e-tests/test-applications/node-profiling-electron/index.electron.js index 7c4246d616a9..d08ac4ecc142 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-electron/index.electron.js +++ b/dev-packages/e2e-tests/test-applications/node-profiling-electron/index.electron.js @@ -4,7 +4,6 @@ const Sentry = require('@sentry/electron/main'); const path = require('node:path'); Sentry.init({ - traceLifecycle: 'static', dsn: 'https://7fa19397baaf433f919fbe02228d5470@o1137848.ingest.sentry.io/6625302', debug: true, tracesSampleRate: 1.0,