Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ lerna-debug.log*
.env.test.local
.env.production.local
.env.local
!functions/.env.local

# temp directory
.temp
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# The emulator loads firebase-functions before the handler module calls Sentry.init().
NODE_OPTIONS=--import=@sentry/node/import
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -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]));
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading