|
1 | 1 | import { expect, test } from '@playwright/test'; |
2 | | -import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; |
| 2 | +import { collectStreamedSpans, getSpanOp, waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; |
3 | 3 |
|
4 | 4 | // FIXME: firebase-functions runs inside the Firebase emulator, where the channel-injection runtime |
5 | 5 | // module hook doesn't transform the emulator-loaded handlers, so no channel spans are produced. |
6 | 6 | // (Firestore in a plain Node process works.) Deferred; tracked separately for a channel firebase- |
7 | 7 | // functions emulator fix. |
8 | 8 | test.fixme('should only call the function once without any extra calls', async () => { |
9 | | - const serverTransactionPromise = waitForTransaction('node-firebase', span => { |
10 | | - return span.transaction === 'firebase.function.http.request'; |
11 | | - }); |
12 | | - |
13 | | - await fetch(`http://localhost:5001/demo-functions/default/helloWorld`); |
| 9 | + const spanPromise = waitForStreamedSpan( |
| 10 | + 'node-firebase', |
| 11 | + span => |
| 12 | + span.is_segment && |
| 13 | + span.name === 'firebase.function.http.request' && |
| 14 | + span.attributes['faas.name']?.value === 'helloWorld', |
| 15 | + ); |
14 | 16 |
|
15 | | - const transactionEvent = await serverTransactionPromise; |
| 17 | + await fetch('http://localhost:5001/demo-functions/default/helloWorld'); |
16 | 18 |
|
17 | | - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); |
18 | | - expect(transactionEvent.contexts).toEqual( |
19 | | - expect.objectContaining({ |
20 | | - trace: expect.objectContaining({ |
21 | | - data: { |
22 | | - 'cloud.project_id': 'demo-functions', |
23 | | - 'faas.name': 'helloWorld', |
24 | | - 'faas.provider': 'firebase', |
25 | | - 'faas.trigger': 'http.request', |
26 | | - 'sentry.kind': 'server', |
27 | | - 'sentry.op': 'http.request', |
28 | | - 'sentry.origin': 'auto.firebase.functions', |
29 | | - 'sentry.sample_rate': expect.any(Number), |
30 | | - 'sentry.segment.name.source': 'route', |
31 | | - }, |
32 | | - op: 'http.request', |
33 | | - origin: 'auto.firebase.functions', |
34 | | - span_id: expect.any(String), |
35 | | - status: 'ok', |
36 | | - trace_id: expect.any(String), |
37 | | - }), |
| 19 | + const span = await spanPromise; |
| 20 | + expect(getSpanOp(span)).toBe('http.request'); |
| 21 | + expect(span).toMatchObject({ |
| 22 | + name: 'firebase.function.http.request', |
| 23 | + status: 'ok', |
| 24 | + span_id: expect.any(String), |
| 25 | + trace_id: expect.any(String), |
| 26 | + attributes: expect.objectContaining({ |
| 27 | + 'cloud.project_id': { value: 'demo-functions', type: 'string' }, |
| 28 | + 'faas.name': { value: 'helloWorld', type: 'string' }, |
| 29 | + 'faas.provider': { value: 'firebase', type: 'string' }, |
| 30 | + 'faas.trigger': { value: 'http.request', type: 'string' }, |
| 31 | + 'sentry.kind': { value: 'server', type: 'string' }, |
| 32 | + 'sentry.origin': { value: 'auto.firebase.functions', type: 'string' }, |
| 33 | + 'sentry.sample_rate': { value: expect.any(Number), type: 'integer' }, |
| 34 | + 'sentry.segment.name.source': { value: 'route', type: 'string' }, |
38 | 35 | }), |
39 | | - ); |
40 | | -}); |
41 | | - |
42 | | -test.fixme('should send failed transaction when the function fails', async () => { |
43 | | - const errorEventPromise = waitForError('node-firebase', () => true); |
44 | | - const serverTransactionPromise = waitForTransaction('node-firebase', span => { |
45 | | - return !!span.transaction; |
46 | 36 | }); |
| 37 | +}); |
47 | 38 |
|
48 | | - await fetch(`http://localhost:5001/demo-functions/default/unhandeledError`); |
| 39 | +test.fixme('should send failed span when the function fails', async () => { |
| 40 | + const errorPromise = waitForError( |
| 41 | + 'node-firebase', |
| 42 | + event => event.exception?.values?.[0]?.value === 'There is an error!', |
| 43 | + ); |
| 44 | + const spanPromise = waitForStreamedSpan( |
| 45 | + 'node-firebase', |
| 46 | + span => |
| 47 | + span.is_segment && |
| 48 | + span.name === 'firebase.function.http.request' && |
| 49 | + span.attributes['faas.name']?.value === 'unhandeledError', |
| 50 | + ); |
49 | 51 |
|
50 | | - const transactionEvent = await serverTransactionPromise; |
51 | | - const errorEvent = await errorEventPromise; |
| 52 | + await fetch('http://localhost:5001/demo-functions/default/unhandeledError'); |
52 | 53 |
|
53 | | - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); |
54 | | - expect(transactionEvent.contexts?.trace?.trace_id).toEqual(errorEvent.contexts?.trace?.trace_id); |
55 | | - expect(errorEvent).toMatchObject({ |
56 | | - exception: { |
57 | | - values: [ |
58 | | - { |
59 | | - type: 'Error', |
60 | | - value: 'There is an error!', |
61 | | - mechanism: { |
62 | | - type: 'auto.firebase.functions', |
63 | | - handled: false, |
64 | | - }, |
65 | | - }, |
66 | | - ], |
67 | | - }, |
68 | | - }); |
| 54 | + const span = await spanPromise; |
| 55 | + const error = await errorPromise; |
| 56 | + expect(span.status).toBe('error'); |
| 57 | + expect(span.trace_id).toBe(error.contexts?.trace?.trace_id); |
| 58 | + expect(error.exception?.values).toEqual([ |
| 59 | + expect.objectContaining({ |
| 60 | + type: 'Error', |
| 61 | + value: 'There is an error!', |
| 62 | + mechanism: { type: 'auto.firebase.functions', handled: false }, |
| 63 | + }), |
| 64 | + ]); |
69 | 65 | }); |
70 | 66 |
|
71 | 67 | test.fixme('should create a document and trigger onDocumentCreated and another with authContext', async () => { |
72 | | - const serverTransactionPromise = waitForTransaction('node-firebase', span => { |
73 | | - return span.transaction === 'firebase.function.http.request'; |
74 | | - }); |
75 | | - |
76 | | - const serverTransactionOnDocumentCreatePromise = waitForTransaction('node-firebase', span => { |
77 | | - return ( |
78 | | - span.transaction === 'firebase.function.firestore.document.created' && |
79 | | - span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreate' |
80 | | - ); |
81 | | - }); |
82 | | - |
83 | | - const serverTransactionOnDocumentWithAuthContextCreatePromise = waitForTransaction('node-firebase', span => { |
84 | | - return ( |
85 | | - span.transaction === 'firebase.function.firestore.document.created' && |
86 | | - span.contexts?.trace?.data?.['faas.name'] === 'onDocumentCreateWithAuthContext' |
87 | | - ); |
88 | | - }); |
89 | | - |
90 | | - await fetch(`http://localhost:5001/demo-functions/default/onCallSomething`); |
| 68 | + const functions = [ |
| 69 | + { name: 'onCallSomething', trigger: 'http.request', children: 3 }, |
| 70 | + { name: 'onDocumentCreate', trigger: 'firestore.document.created', children: 2 }, |
| 71 | + { name: 'onDocumentCreateWithAuthContext', trigger: 'firestore.document.created', children: 0 }, |
| 72 | + ]; |
| 73 | + const spanPromises = functions.map(({ name, children }) => |
| 74 | + collectStreamedSpans( |
| 75 | + 'node-firebase', |
| 76 | + spans => |
| 77 | + spans.some(span => span.is_segment && span.attributes['faas.name']?.value === name) && |
| 78 | + spans.filter(span => !span.is_segment).length >= children, |
| 79 | + ), |
| 80 | + ); |
91 | 81 |
|
92 | | - const transactionEvent = await serverTransactionPromise; |
93 | | - const transactionEventOnDocumentCreate = await serverTransactionOnDocumentCreatePromise; |
94 | | - const transactionEventOnDocumentWithAuthContextCreate = await serverTransactionOnDocumentWithAuthContextCreatePromise; |
| 82 | + await fetch('http://localhost:5001/demo-functions/default/onCallSomething'); |
95 | 83 |
|
96 | | - expect(transactionEvent.transaction).toEqual('firebase.function.http.request'); |
97 | | - expect(transactionEvent.contexts?.trace).toEqual({ |
98 | | - data: { |
99 | | - 'cloud.project_id': 'demo-functions', |
100 | | - 'faas.name': 'onCallSomething', |
101 | | - 'faas.provider': 'firebase', |
102 | | - 'faas.trigger': 'http.request', |
103 | | - 'sentry.kind': 'server', |
104 | | - 'sentry.op': 'http.request', |
105 | | - 'sentry.origin': 'auto.firebase.functions', |
106 | | - 'sentry.sample_rate': expect.any(Number), |
107 | | - 'sentry.segment.name.source': 'route', |
108 | | - }, |
109 | | - op: 'http.request', |
110 | | - origin: 'auto.firebase.functions', |
111 | | - span_id: expect.any(String), |
112 | | - status: 'ok', |
113 | | - trace_id: expect.any(String), |
114 | | - }); |
115 | | - expect(transactionEvent.spans).toHaveLength(3); |
116 | | - expect(transactionEventOnDocumentCreate.contexts?.trace).toEqual({ |
117 | | - data: { |
118 | | - 'cloud.project_id': 'demo-functions', |
119 | | - 'faas.name': 'onDocumentCreate', |
120 | | - 'faas.provider': 'firebase', |
121 | | - 'faas.trigger': 'firestore.document.created', |
122 | | - 'sentry.kind': 'server', |
123 | | - 'sentry.op': expect.any(String), |
124 | | - 'sentry.origin': 'auto.firebase.functions', |
125 | | - 'sentry.sample_rate': expect.any(Number), |
126 | | - 'sentry.segment.name.source': 'route', |
127 | | - }, |
128 | | - op: expect.any(String), |
129 | | - origin: 'auto.firebase.functions', |
130 | | - span_id: expect.any(String), |
131 | | - status: 'ok', |
132 | | - trace_id: expect.any(String), |
133 | | - }); |
134 | | - expect(transactionEventOnDocumentCreate.spans).toHaveLength(2); |
135 | | - expect(transactionEventOnDocumentWithAuthContextCreate.contexts?.trace).toEqual({ |
136 | | - data: { |
137 | | - 'cloud.project_id': 'demo-functions', |
138 | | - 'faas.name': 'onDocumentCreateWithAuthContext', |
139 | | - 'faas.provider': 'firebase', |
140 | | - 'faas.trigger': 'firestore.document.created', |
141 | | - 'sentry.kind': 'server', |
142 | | - 'sentry.op': expect.any(String), |
143 | | - 'sentry.origin': 'auto.firebase.functions', |
144 | | - 'sentry.sample_rate': expect.any(Number), |
145 | | - 'sentry.segment.name.source': 'route', |
146 | | - }, |
147 | | - op: expect.any(String), |
148 | | - origin: 'auto.firebase.functions', |
149 | | - span_id: expect.any(String), |
150 | | - status: 'ok', |
151 | | - trace_id: expect.any(String), |
| 84 | + const traces = await Promise.all(spanPromises); |
| 85 | + functions.forEach(({ name, trigger, children }, index) => { |
| 86 | + const spans = traces[index]!; |
| 87 | + const segment = spans.find(span => span.is_segment && span.attributes['faas.name']?.value === name)!; |
| 88 | + expect(segment).toMatchObject({ |
| 89 | + name: `firebase.function.${trigger}`, |
| 90 | + status: 'ok', |
| 91 | + span_id: expect.any(String), |
| 92 | + trace_id: expect.any(String), |
| 93 | + attributes: expect.objectContaining({ |
| 94 | + 'cloud.project_id': { value: 'demo-functions', type: 'string' }, |
| 95 | + 'faas.name': { value: name, type: 'string' }, |
| 96 | + 'faas.provider': { value: 'firebase', type: 'string' }, |
| 97 | + 'faas.trigger': { value: trigger, type: 'string' }, |
| 98 | + 'sentry.kind': { value: 'server', type: 'string' }, |
| 99 | + 'sentry.op': { value: expect.any(String), type: 'string' }, |
| 100 | + 'sentry.origin': { value: 'auto.firebase.functions', type: 'string' }, |
| 101 | + 'sentry.sample_rate': { value: expect.any(Number), type: 'integer' }, |
| 102 | + 'sentry.segment.name.source': { value: 'route', type: 'string' }, |
| 103 | + }), |
| 104 | + }); |
| 105 | + expect(spans.filter(span => !span.is_segment)).toHaveLength(children); |
152 | 106 | }); |
153 | | - expect(transactionEventOnDocumentWithAuthContextCreate.spans).toHaveLength(0); |
154 | 107 | }); |
0 commit comments