1
- import * as dns from 'dns' ;
2
- import { EventEmitter } from 'events' ;
3
- import * as fs from 'fs' ;
4
- import * as path from 'path' ;
5
- import { PubSub } from '@google-cloud/pubsub' ;
6
- import * as http2 from 'http2' ;
7
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
8
- // @ts -ignore ESM/CJS interop issue
9
- import nock from 'nock' ;
10
- import { describe , vi , beforeEach , test , expect , type Mock , afterAll , afterEach } from 'vitest' ;
11
-
12
- import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core' ;
13
- import { NodeClient , createTransport , setCurrentClient } from '@sentry/node' ;
14
- import { googleCloudGrpcIntegration } from '../../src/integrations/google-cloud-grpc' ;
15
-
16
- vi . mock ( 'dns' ) ;
17
- vi . mock ( 'http2' ) ;
18
-
19
- const spyConnect = vi . spyOn ( http2 , 'connect' ) ;
1
+ import { vi , describe , beforeEach , test , expect } from 'vitest' ;
2
+ import { NodeClient } from '@sentry/node' ;
3
+ import { createTransport } from '@sentry/core' ;
4
+ import { setCurrentClient , SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core' ;
5
+ import { googleCloudGrpcIntegration , fillGrpcFunction } from '../../src/integrations/google-cloud-grpc' ;
6
+ import type { GrpcFunctionObject , Stub , GrpcFunction } from '../../src/integrations/google-cloud-grpc' ;
7
+
20
8
const mockSpanEnd = vi . fn ( ) ;
21
- const mockStartInactiveSpan = vi . fn ( spanArgs => ( { ...spanArgs } ) ) ;
9
+ const mockStartInactiveSpan = vi . fn ( ) ;
10
+ const mockFill = vi . fn ( ) ;
11
+
12
+ let mockClient : NodeClient ;
13
+
14
+ vi . mock ( '@sentry/core' , async ( ) => {
15
+ const original = await vi . importActual ( '@sentry/core' ) ;
16
+ return {
17
+ ...original ,
18
+ fill : ( obj : any , name : string , replacement : any ) => {
19
+ mockFill ( obj , name , replacement ) ;
20
+ obj [ name ] = replacement ( obj [ name ] ) ;
21
+ } ,
22
+ getClient : ( ) => mockClient ,
23
+ } ;
24
+ } ) ;
22
25
23
26
vi . mock ( '@sentry/node' , async ( ) => {
24
- // eslint-disable-next-line @typescript-eslint/consistent-type-imports
25
- const original = ( await vi . importActual ( '@sentry/node' ) ) as typeof import ( '@sentry/node' ) ;
27
+ const original = await vi . importActual ( '@sentry/node' ) ;
26
28
return {
27
29
...original ,
28
30
startInactiveSpan : ( ctx : unknown ) => {
@@ -32,130 +34,177 @@ vi.mock('@sentry/node', async () => {
32
34
} ;
33
35
} ) ;
34
36
35
- /** Fake HTTP2 stream */
36
- class FakeStream extends EventEmitter {
37
- public rstCode : number = 0 ;
38
- close ( ) {
39
- this . emit ( 'end' ) ;
40
- this . emit ( 'close' ) ;
41
- }
42
- end ( ) { }
43
- pause ( ) { }
44
- resume ( ) { }
45
- write ( _data : Buffer , cb : CallableFunction ) {
46
- process . nextTick ( cb , null ) ;
47
- }
48
- }
49
-
50
- /** Fake HTTP2 session for GRPC */
51
- class FakeSession extends EventEmitter {
52
- public socket : EventEmitter = new EventEmitter ( ) ;
53
- public request : Mock = vi . fn ( ) ;
54
- ping ( ) { }
55
- mockRequest ( fn : ( stream : FakeStream ) => void ) : FakeStream {
56
- const stream = new FakeStream ( ) ;
57
- this . request . mockImplementationOnce ( ( ) => {
58
- process . nextTick ( fn , stream ) ;
59
- return stream ;
60
- } ) ;
61
- return stream ;
62
- }
63
- mockUnaryRequest ( responseData : Buffer ) {
64
- this . mockRequest ( stream => {
65
- stream . emit (
66
- 'response' ,
67
- { ':status' : 200 , 'content-type' : 'application/grpc' , 'content-disposition' : 'attachment' } ,
68
- 4 ,
69
- ) ;
70
- stream . emit ( 'data' , responseData ) ;
71
- stream . emit ( 'trailers' , { 'grpc-status' : '0' , 'content-disposition' : 'attachment' } ) ;
72
- } ) ;
73
- }
74
- close ( ) {
75
- this . emit ( 'close' ) ;
76
- this . socket . emit ( 'close' ) ;
77
- }
78
- ref ( ) { }
79
- unref ( ) { }
37
+ // Need to override mock because the integration loads google-gax as a CJS file
38
+ async function mock ( mockedUri : string , stub : any ) {
39
+ // @ts -expect-error we are using import on purpose
40
+ const { Module } = await import ( 'module' ) ;
41
+
42
+ // @ts -expect-error test
43
+ Module . _load_original = Module . _load ;
44
+ // @ts -expect-error test
45
+ Module . _load = ( uri , parent ) => {
46
+ if ( uri === mockedUri ) return stub ;
47
+ // @ts -expect-error test
48
+ return Module . _load_original ( uri , parent ) ;
49
+ } ;
80
50
}
81
51
82
- function mockHttp2Session ( ) : FakeSession {
83
- const session = new FakeSession ( ) ;
84
- spyConnect . mockImplementationOnce ( ( ) => {
85
- process . nextTick ( ( ) => session . emit ( 'connect' ) ) ;
86
- return session as unknown as http2 . ClientHttp2Session ;
87
- } ) ;
88
- return session ;
89
- }
52
+ vi . hoisted (
53
+ ( ) =>
54
+ void mock ( 'google-gax' , {
55
+ GrpcClient : {
56
+ prototype : {
57
+ createStub : vi . fn ( ) ,
58
+ } ,
59
+ } ,
60
+ } ) ,
61
+ ) ;
90
62
91
63
describe ( 'GoogleCloudGrpc tracing' , ( ) => {
92
- const mockClient = new NodeClient ( {
93
- tracesSampleRate : 1.0 ,
94
- integrations : [ ] ,
95
- dsn : 'https://withAWSServices@domain/123' ,
96
- transport : ( ) => createTransport ( { recordDroppedEvent : ( ) => undefined } , _ => Promise . resolve ( { } ) ) ,
97
- stackParser : ( ) => [ ] ,
98
- } ) ;
64
+ beforeEach ( ( ) => {
65
+ mockClient = new NodeClient ( {
66
+ tracesSampleRate : 1.0 ,
67
+ integrations : [ ] ,
68
+ dsn : 'https://withAWSServices@domain/123' ,
69
+ transport : ( ) => createTransport ( { recordDroppedEvent : ( ) => undefined } , _ => Promise . resolve ( { } ) ) ,
70
+ stackParser : ( ) => [ ] ,
71
+ } ) ;
99
72
100
- const integration = googleCloudGrpcIntegration ( ) ;
101
- mockClient . addIntegration ( integration ) ;
73
+ const integration = googleCloudGrpcIntegration ( ) ;
74
+ mockClient . addIntegration ( integration ) ;
75
+ integration . setup ?.( mockClient ) ;
102
76
103
- beforeEach ( ( ) => {
104
- nock ( 'https://www.googleapis.com' ) . post ( '/oauth2/v4/token' ) . reply ( 200 , [ ] ) ;
105
77
setCurrentClient ( mockClient ) ;
106
78
mockSpanEnd . mockClear ( ) ;
107
79
mockStartInactiveSpan . mockClear ( ) ;
80
+ mockFill . mockClear ( ) ;
108
81
} ) ;
109
82
110
- afterAll ( ( ) => {
111
- nock . restore ( ) ;
112
- spyConnect . mockRestore ( ) ;
113
- } ) ;
114
-
115
- // We use google cloud pubsub as an example of grpc service for which we can trace requests.
116
- describe ( 'pubsub' , ( ) => {
117
- // @ts -expect-error see "Why @ts-expect-error" note
118
- const dnsLookup = dns . lookup as Mock ;
119
- // @ts -expect-error see "Why @ts-expect-error" note
120
- const resolveTxt = dns . resolveTxt as Mock ;
121
- dnsLookup . mockImplementation ( ( hostname , ...args ) => {
122
- expect ( hostname ) . toEqual ( 'pubsub.googleapis.com' ) ;
123
- process . nextTick ( args [ args . length - 1 ] , null , [ { address : '0.0.0.0' , family : 4 } ] ) ;
124
- } ) ;
125
- resolveTxt . mockImplementation ( ( hostname , cb ) => {
126
- expect ( hostname ) . toEqual ( 'pubsub.googleapis.com' ) ;
127
- process . nextTick ( cb , null , [ ] ) ;
83
+ describe ( 'setup' , ( ) => {
84
+ test ( 'integration name is correct' , ( ) => {
85
+ const integration = googleCloudGrpcIntegration ( ) ;
86
+ expect ( integration . name ) . toBe ( 'GoogleCloudGrpc' ) ;
128
87
} ) ;
129
88
130
- const pubsub = new PubSub ( {
131
- credentials : {
132
- client_email : 'client@email' ,
133
- private_key : fs . readFileSync ( path . resolve ( __dirname , 'private.pem' ) ) . toString ( ) ,
134
- } ,
135
- projectId : 'project-id' ,
89
+ test ( 'setupOnce patches GrpcClient.createStub' , ( ) => {
90
+ const mockCreateStub = vi . fn ( ) ;
91
+ const mockGrpcClient = {
92
+ prototype : {
93
+ createStub : mockCreateStub ,
94
+ } ,
95
+ } ;
96
+
97
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
98
+ require ( 'google-gax' ) . GrpcClient = mockGrpcClient ;
99
+
100
+ const integration = googleCloudGrpcIntegration ( ) ;
101
+ integration . setupOnce ?.( ) ;
102
+ expect ( mockCreateStub ) . toBeDefined ( ) ;
136
103
} ) ;
137
104
138
- afterEach ( ( ) => {
139
- dnsLookup . mockReset ( ) ;
140
- resolveTxt . mockReset ( ) ;
105
+ test ( 'setupOnce throws when google-gax is not available and not optional' , ( ) => {
106
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
107
+ require ( 'google-gax' ) . GrpcClient = undefined ;
108
+
109
+ const integration = googleCloudGrpcIntegration ( ) ;
110
+ expect ( ( ) => integration . setupOnce ?.( ) ) . toThrow ( ) ;
141
111
} ) ;
142
112
143
- afterAll ( async ( ) => {
144
- await pubsub . close ( ) ;
113
+ test ( 'setupOnce does not throw when google-gax is not available and optional' , ( ) => {
114
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
115
+ require ( 'google-gax' ) . GrpcClient = undefined ;
116
+
117
+ const optionalIntegration = googleCloudGrpcIntegration ( { optional : true } ) ;
118
+ expect ( ( ) => optionalIntegration . setupOnce ?.( ) ) . not . toThrow ( ) ;
145
119
} ) ;
120
+ } ) ;
146
121
147
- test ( 'publish' , async ( ) => {
148
- mockHttp2Session ( ) . mockUnaryRequest ( Buffer . from ( '00000000120a1031363337303834313536363233383630' , 'hex' ) ) ;
149
- const resp = await pubsub . topic ( 'nicetopic' ) . publish ( Buffer . from ( 'data' ) ) ;
150
- expect ( resp ) . toEqual ( '1637084156623860' ) ;
151
- expect ( mockStartInactiveSpan ) . toBeCalledWith ( {
152
- op : 'grpc.pubsub' ,
122
+ describe ( 'fillGrpcFunction' , ( ) => {
123
+ test ( 'patches unary call methods with tracing' , ( ) => {
124
+ const mockStub : Stub = {
125
+ unaryMethod : Object . assign ( vi . fn ( ) , {
126
+ requestStream : false ,
127
+ responseStream : false ,
128
+ originalName : 'unaryMethod' ,
129
+ } as GrpcFunctionObject ) ,
130
+ } ;
131
+
132
+ const mockEventEmitter = {
133
+ on : vi . fn ( ) ,
134
+ } ;
135
+
136
+ ( mockStub . unaryMethod as any ) . apply = vi . fn ( ) . mockReturnValue ( mockEventEmitter ) ;
137
+
138
+ fillGrpcFunction ( mockStub , 'test-service' , 'unaryMethod' ) ;
139
+
140
+ const result = ( mockStub . unaryMethod as GrpcFunction ) ( ) ;
141
+ expect ( result ) . toBe ( mockEventEmitter ) ;
142
+ expect ( mockEventEmitter . on ) . toHaveBeenCalledWith ( 'status' , expect . any ( Function ) ) ;
143
+ expect ( mockStartInactiveSpan ) . toHaveBeenCalledWith ( {
144
+ name : 'unary call unaryMethod' ,
145
+ onlyIfParent : true ,
146
+ op : 'grpc.test-service' ,
153
147
attributes : {
154
148
[ SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ] : 'auto.grpc.serverless' ,
155
149
} ,
156
- name : 'unary call publish' ,
157
- onlyIfParent : true ,
158
150
} ) ;
159
151
} ) ;
152
+
153
+ test ( 'does not patch non-unary call methods' , ( ) => {
154
+ const mockStub : Stub = {
155
+ clientStreamMethod : Object . assign ( vi . fn ( ) , {
156
+ requestStream : true ,
157
+ responseStream : false ,
158
+ originalName : 'clientStreamMethod' ,
159
+ } as GrpcFunctionObject ) ,
160
+ serverStreamMethod : Object . assign ( vi . fn ( ) , {
161
+ requestStream : false ,
162
+ responseStream : true ,
163
+ originalName : 'serverStreamMethod' ,
164
+ } as GrpcFunctionObject ) ,
165
+ bidiStreamMethod : Object . assign ( vi . fn ( ) , {
166
+ requestStream : true ,
167
+ responseStream : true ,
168
+ originalName : 'bidiStreamMethod' ,
169
+ } as GrpcFunctionObject ) ,
170
+ } ;
171
+
172
+ fillGrpcFunction ( mockStub , 'test-service' , 'clientStreamMethod' ) ;
173
+ fillGrpcFunction ( mockStub , 'test-service' , 'serverStreamMethod' ) ;
174
+ fillGrpcFunction ( mockStub , 'test-service' , 'bidiStreamMethod' ) ;
175
+
176
+ expect ( mockStartInactiveSpan ) . not . toHaveBeenCalled ( ) ;
177
+ } ) ;
178
+
179
+ test ( 'does not patch non-function properties' , ( ) => {
180
+ const mockStub : Stub = {
181
+ nonFunction : Object . assign ( vi . fn ( ) , {
182
+ requestStream : false ,
183
+ responseStream : false ,
184
+ originalName : 'nonFunction' ,
185
+ } as GrpcFunctionObject ) ,
186
+ } ;
187
+
188
+ fillGrpcFunction ( mockStub , 'test-service' , 'nonFunction' ) ;
189
+ expect ( mockStartInactiveSpan ) . not . toHaveBeenCalled ( ) ;
190
+ } ) ;
191
+
192
+ test ( 'does not patch methods when return value is not an EventEmitter' , ( ) => {
193
+ const mockStub : Stub = {
194
+ unaryMethod : Object . assign ( vi . fn ( ) , {
195
+ requestStream : false ,
196
+ responseStream : false ,
197
+ originalName : 'unaryMethod' ,
198
+ } as GrpcFunctionObject ) ,
199
+ } ;
200
+
201
+ ( mockStub . unaryMethod as any ) . apply = vi . fn ( ) . mockReturnValue ( { notAnEventEmitter : true } ) ;
202
+
203
+ fillGrpcFunction ( mockStub , 'test-service' , 'unaryMethod' ) ;
204
+
205
+ const result = ( mockStub . unaryMethod as GrpcFunction ) ( ) ;
206
+ expect ( result ) . toEqual ( { notAnEventEmitter : true } ) ;
207
+ expect ( mockStartInactiveSpan ) . not . toHaveBeenCalled ( ) ;
208
+ } ) ;
160
209
} ) ;
161
210
} ) ;
0 commit comments