Skip to content

Commit 990afcf

Browse files
committed
ref(serverless): Convert GoogleCloudGrpc to function
1 parent bfb0fa5 commit 990afcf

File tree

5 files changed

+100
-70
lines changed

5 files changed

+100
-70
lines changed

packages/serverless/src/awsservices.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface AWSService {
1818

1919
const INTEGRATION_NAME = 'AWSServices';
2020

21-
export const SETUP_CLIENTS = new WeakMap<Client, boolean>();
21+
const SETUP_CLIENTS = new WeakMap<Client, boolean>();
2222

2323
const _awsServicesIntegration = ((options: { optional?: boolean } = {}) => {
2424
const optional = options.optional || false;

packages/serverless/src/gcpfunction/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from '@sentry/node';
88
import type { Integration, Options, SdkMetadata } from '@sentry/types';
99

10-
import { GoogleCloudGrpc } from '../google-cloud-grpc';
10+
import { googleCloudGrpcIntegration } from '../google-cloud-grpc';
1111
import { GoogleCloudHttp } from '../google-cloud-http';
1212

1313
export * from './http';
@@ -19,15 +19,15 @@ export const defaultIntegrations: Integration[] = [
1919
// eslint-disable-next-line deprecation/deprecation
2020
...defaultNodeIntegrations,
2121
new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
22-
new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
22+
googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
2323
];
2424

2525
/** Get the default integrations for the GCP SDK. */
2626
export function getDefaultIntegrations(options: Options): Integration[] {
2727
return [
2828
...getDefaultNodeIntegrations(options),
2929
new GoogleCloudHttp({ optional: true }), // We mark this integration optional since '@google-cloud/common' module could be missing.
30-
new GoogleCloudGrpc({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
30+
googleCloudGrpcIntegration({ optional: true }), // We mark this integration optional since 'google-gax' module could be missing.
3131
];
3232
}
3333

packages/serverless/src/google-cloud-grpc.ts

Lines changed: 54 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { EventEmitter } from 'events';
2+
import { convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core';
23
import { startInactiveSpan } from '@sentry/node';
3-
import type { Integration } from '@sentry/types';
4+
import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types';
45
import { fill } from '@sentry/utils';
56

67
interface GrpcFunction extends CallableFunction {
@@ -25,45 +26,49 @@ interface Stub {
2526
[key: string]: GrpcFunctionObject;
2627
}
2728

28-
/** Google Cloud Platform service requests tracking for GRPC APIs */
29-
export class GoogleCloudGrpc implements Integration {
30-
/**
31-
* @inheritDoc
32-
*/
33-
public static id: string = 'GoogleCloudGrpc';
29+
const SERVICE_PATH_REGEX = /^(\w+)\.googleapis.com$/;
3430

35-
/**
36-
* @inheritDoc
37-
*/
38-
public name: string;
31+
const INTEGRATION_NAME = 'GoogleCloudGrpc';
3932

40-
private readonly _optional: boolean;
33+
const SETUP_CLIENTS = new WeakMap<Client, boolean>();
4134

42-
public constructor(options: { optional?: boolean } = {}) {
43-
this.name = GoogleCloudGrpc.id;
35+
const _googleCloudGrpcIntegration = ((options: { optional?: boolean } = {}) => {
36+
const optional = options.optional || false;
37+
return {
38+
name: INTEGRATION_NAME,
39+
setupOnce() {
40+
try {
41+
// eslint-disable-next-line @typescript-eslint/no-var-requires
42+
const gaxModule = require('google-gax');
43+
fill(
44+
gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
45+
'createStub',
46+
wrapCreateStub,
47+
);
48+
} catch (e) {
49+
if (!optional) {
50+
throw e;
51+
}
52+
}
53+
},
54+
setup(client) {
55+
SETUP_CLIENTS.set(client, true);
56+
},
57+
};
58+
}) satisfies IntegrationFn;
4459

45-
this._optional = options.optional || false;
46-
}
60+
export const googleCloudGrpcIntegration = defineIntegration(_googleCloudGrpcIntegration);
4761

48-
/**
49-
* @inheritDoc
50-
*/
51-
public setupOnce(): void {
52-
try {
53-
// eslint-disable-next-line @typescript-eslint/no-var-requires
54-
const gaxModule = require('google-gax');
55-
fill(
56-
gaxModule.GrpcClient.prototype, // eslint-disable-line @typescript-eslint/no-unsafe-member-access
57-
'createStub',
58-
wrapCreateStub,
59-
);
60-
} catch (e) {
61-
if (!this._optional) {
62-
throw e;
63-
}
64-
}
65-
}
66-
}
62+
/**
63+
* Google Cloud Platform service requests tracking for GRPC APIs.
64+
*
65+
* @deprecated Use `googleCloudGrpcIntegration()` instead.
66+
*/
67+
// eslint-disable-next-line deprecation/deprecation
68+
export const GoogleCloudGrpc = convertIntegrationFnToClass(
69+
INTEGRATION_NAME,
70+
googleCloudGrpcIntegration,
71+
) as IntegrationClass<Integration>;
6772

6873
/** Returns a wrapped function that returns a stub with tracing enabled */
6974
function wrapCreateStub(origCreate: CreateStubFunc): CreateStubFunc {
@@ -107,23 +112,26 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str
107112
if (typeof ret?.on !== 'function') {
108113
return ret;
109114
}
110-
const span = startInactiveSpan({
111-
name: `${callType} ${methodName}`,
112-
op: `grpc.${serviceIdentifier}`,
113-
origin: 'auto.grpc.serverless',
114-
});
115-
ret.on('status', () => {
116-
if (span) {
117-
span.end();
118-
}
119-
});
115+
// only instrument if integration is active on client
116+
if (SETUP_CLIENTS.has(getClient() as Client)) {
117+
const span = startInactiveSpan({
118+
name: `${callType} ${methodName}`,
119+
op: `grpc.${serviceIdentifier}`,
120+
origin: 'auto.grpc.serverless',
121+
});
122+
ret.on('status', () => {
123+
if (span) {
124+
span.end();
125+
}
126+
});
127+
}
120128
return ret;
121129
},
122130
);
123131
}
124132

125133
/** Identifies service by its address */
126134
function identifyService(servicePath: string): string {
127-
const match = servicePath.match(/^(\w+)\.googleapis.com$/);
135+
const match = servicePath.match(SERVICE_PATH_REGEX);
128136
return match ? match[1] : servicePath;
129137
}

packages/serverless/test/awsservices.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import * as SentryNode from '@sentry/node';
1+
import { NodeClient, createTransport, setCurrentClient } from '@sentry/node';
22
import * as AWS from 'aws-sdk';
33
import * as nock from 'nock';
44

@@ -18,22 +18,22 @@ jest.mock('@sentry/node', () => {
1818
});
1919

2020
describe('awsServicesIntegration', () => {
21-
const mockClient = new SentryNode.NodeClient({
21+
const mockClient = new NodeClient({
2222
tracesSampleRate: 1.0,
2323
integrations: [],
2424
dsn: 'https://withAWSServices@domain/123',
25-
transport: () => SentryNode.createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
25+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
2626
stackParser: () => [],
2727
});
2828

2929
const integration = awsServicesIntegration();
3030
mockClient.addIntegration(integration);
3131

32-
const mockClientWithoutIntegration = new SentryNode.NodeClient({
32+
const mockClientWithoutIntegration = new NodeClient({
3333
tracesSampleRate: 1.0,
3434
integrations: [],
3535
dsn: 'https://withoutAWSServices@domain/123',
36-
transport: () => SentryNode.createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
36+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
3737
stackParser: () => [],
3838
});
3939

@@ -42,7 +42,7 @@ describe('awsServicesIntegration', () => {
4242
});
4343

4444
beforeEach(() => {
45-
SentryNode.setCurrentClient(mockClient);
45+
setCurrentClient(mockClient);
4646
mockSpanEnd.mockClear();
4747
mockStartInactiveSpan.mockClear();
4848
});
@@ -64,7 +64,7 @@ describe('awsServicesIntegration', () => {
6464
});
6565

6666
test('getObject with integration-less client', async () => {
67-
SentryNode.setCurrentClient(mockClientWithoutIntegration);
67+
setCurrentClient(mockClientWithoutIntegration);
6868
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
6969
await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise();
7070
expect(mockStartInactiveSpan).not.toBeCalled();
@@ -86,7 +86,7 @@ describe('awsServicesIntegration', () => {
8686
});
8787

8888
test('getObject with callback with integration-less client', done => {
89-
SentryNode.setCurrentClient(mockClientWithoutIntegration);
89+
setCurrentClient(mockClientWithoutIntegration);
9090
expect.assertions(1);
9191
nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents');
9292
s3.getObject({ Bucket: 'foo', Key: 'bar' }, () => {
@@ -112,7 +112,7 @@ describe('awsServicesIntegration', () => {
112112
});
113113

114114
test('invoke with integration-less client', async () => {
115-
SentryNode.setCurrentClient(mockClientWithoutIntegration);
115+
setCurrentClient(mockClientWithoutIntegration);
116116
nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply');
117117
await lambda.invoke({ FunctionName: 'foo' }).promise();
118118
expect(mockStartInactiveSpan).not.toBeCalled();

packages/serverless/test/google-cloud-grpc.test.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,27 @@ import { EventEmitter } from 'events';
55
import * as fs from 'fs';
66
import * as path from 'path';
77
import { PubSub } from '@google-cloud/pubsub';
8-
import * as SentryNode from '@sentry/node';
98
import * as http2 from 'http2';
109
import * as nock from 'nock';
1110

12-
import { GoogleCloudGrpc } from '../src/google-cloud-grpc';
11+
import { NodeClient, createTransport, setCurrentClient } from '@sentry/node';
12+
import { googleCloudGrpcIntegration } from '../src/google-cloud-grpc';
1313

1414
const spyConnect = jest.spyOn(http2, 'connect');
1515

16+
const mockSpanEnd = jest.fn();
17+
const mockStartInactiveSpan = jest.fn(spanArgs => ({ ...spanArgs }));
18+
19+
jest.mock('@sentry/node', () => {
20+
return {
21+
...jest.requireActual('@sentry/node'),
22+
startInactiveSpan: (ctx: unknown) => {
23+
mockStartInactiveSpan(ctx);
24+
return { end: mockSpanEnd };
25+
},
26+
};
27+
});
28+
1629
/** Fake HTTP2 stream */
1730
class FakeStream extends EventEmitter {
1831
public rstCode: number = 0;
@@ -70,18 +83,24 @@ function mockHttp2Session(): FakeSession {
7083
}
7184

7285
describe('GoogleCloudGrpc tracing', () => {
73-
beforeAll(() => {
74-
new GoogleCloudGrpc().setupOnce();
86+
const mockClient = new NodeClient({
87+
tracesSampleRate: 1.0,
88+
integrations: [],
89+
dsn: 'https://withAWSServices@domain/123',
90+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})),
91+
stackParser: () => [],
7592
});
7693

94+
const integration = googleCloudGrpcIntegration();
95+
mockClient.addIntegration(integration);
96+
7797
beforeEach(() => {
7898
nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(200, []);
99+
setCurrentClient(mockClient);
100+
mockSpanEnd.mockClear();
101+
mockStartInactiveSpan.mockClear();
79102
});
80-
afterEach(() => {
81-
// @ts-expect-error see "Why @ts-expect-error" note
82-
SentryNode.resetMocks();
83-
spyConnect.mockClear();
84-
});
103+
85104
afterAll(() => {
86105
nock.restore();
87106
spyConnect.mockRestore();
@@ -115,16 +134,19 @@ describe('GoogleCloudGrpc tracing', () => {
115134
resolveTxt.mockReset();
116135
});
117136

137+
afterAll(async () => {
138+
await pubsub.close();
139+
});
140+
118141
test('publish', async () => {
119142
mockHttp2Session().mockUnaryRequest(Buffer.from('00000000120a1031363337303834313536363233383630', 'hex'));
120143
const resp = await pubsub.topic('nicetopic').publish(Buffer.from('data'));
121144
expect(resp).toEqual('1637084156623860');
122-
expect(SentryNode.startInactiveSpan).toBeCalledWith({
145+
expect(mockStartInactiveSpan).toBeCalledWith({
123146
op: 'grpc.pubsub',
124147
origin: 'auto.grpc.serverless',
125148
name: 'unary call publish',
126149
});
127-
await pubsub.close();
128150
});
129151
});
130152
});

0 commit comments

Comments
 (0)