|
1 |
| -import { |
2 |
| - ApolloServerPlugin as ApolloPlugin, |
3 |
| - GraphQLRequestExecutionListener as ExecutionListener, |
4 |
| - GraphQLRequestListener as Listener, |
5 |
| -} from '@apollo/server'; |
6 |
| -import { Plugin } from '@nestjs/apollo'; |
7 | 1 | import { FieldMiddleware } from '@nestjs/graphql';
|
| 2 | +import { createHash } from 'crypto'; |
8 | 3 | import { GraphQLResolveInfo as ResolveInfo, ResponsePath } from 'graphql';
|
9 |
| -import { GqlContextType as ContextType } from '~/common'; |
10 | 4 | import { Segment, TracingService } from '../tracing';
|
| 5 | +import { Plugin } from './plugin.decorator'; |
11 | 6 |
|
12 | 7 | @Plugin()
|
13 |
| -export class GraphqlTracingPlugin implements ApolloPlugin<ContextType> { |
| 8 | +export class GraphqlTracingPlugin { |
14 | 9 | constructor(private readonly tracing: TracingService) {}
|
15 | 10 |
|
16 |
| - async requestDidStart(): Promise<Listener<ContextType>> { |
17 |
| - return { |
18 |
| - executionDidStart: async ( |
19 |
| - reqContext, |
20 |
| - ): Promise<ExecutionListener<ContextType>> => { |
21 |
| - let segment: Segment; |
22 |
| - try { |
23 |
| - segment = this.tracing.rootSegment; |
24 |
| - } catch (e) { |
25 |
| - return {}; |
26 |
| - } |
| 11 | + onExecute: Plugin['onExecute'] = ({ args }) => { |
| 12 | + const { operationName, contextValue } = args; |
| 13 | + const { operation, session$, params } = contextValue; |
| 14 | + |
| 15 | + let segment: Segment; |
| 16 | + try { |
| 17 | + segment = this.tracing.rootSegment; |
| 18 | + } catch (e) { |
| 19 | + return {}; |
| 20 | + } |
| 21 | + |
| 22 | + segment.name = |
| 23 | + operationName ?? |
| 24 | + (params.query |
| 25 | + ? createHash('sha256').update(params.query).digest('hex') |
| 26 | + : undefined); |
| 27 | + segment.addAnnotation(operation.operation, true); |
27 | 28 |
|
28 |
| - segment.name = reqContext.operationName ?? reqContext.queryHash; |
29 |
| - segment.addAnnotation(reqContext.operation.operation, true); |
| 29 | + // Append operation name to url since all gql requests hit a single http endpoint |
| 30 | + if ( |
| 31 | + // Check if http middleware is present, confirming this is a root subsegment |
| 32 | + (segment as any).http?.request && |
| 33 | + // Confirm operation caller didn't do it themselves. |
| 34 | + // They should, but it's not currently required. |
| 35 | + !(segment as any).http.request.url.endsWith(segment.name) |
| 36 | + ) { |
| 37 | + // @ts-expect-error xray library types suck |
| 38 | + (segment.http.request.url as string) += '/' + segment.name; |
| 39 | + } |
30 | 40 |
|
31 |
| - // Append operation name to url since all gql requests hit a single http endpoint |
32 |
| - if ( |
33 |
| - // Check if http middleware is present, confirming this is a root subsegment |
34 |
| - (segment as any).http?.request && |
35 |
| - // Confirm operation caller didn't do it themselves. |
36 |
| - // They should, but it's not currently required. |
37 |
| - !(segment as any).http.request.url.endsWith(segment.name) |
38 |
| - ) { |
39 |
| - // @ts-expect-error xray library types suck |
40 |
| - (segment.http.request.url as string) += '/' + segment.name; |
| 41 | + return { |
| 42 | + onExecuteDone: () => { |
| 43 | + const userId = session$.value?.userId; |
| 44 | + if (userId) { |
| 45 | + segment.setUser?.(userId); |
41 | 46 | }
|
42 | 47 |
|
43 | 48 | return {
|
44 |
| - executionDidEnd: async (err) => { |
45 |
| - const userId = reqContext.contextValue.session$.value?.userId; |
46 |
| - if (userId) { |
47 |
| - segment.setUser?.(userId); |
48 |
| - } |
49 |
| - |
50 |
| - if (err) { |
51 |
| - segment.addError(err); |
| 49 | + onNext: ({ result }) => { |
| 50 | + for (const error of result.errors ?? []) { |
| 51 | + segment.addError(error); |
52 | 52 | }
|
53 | 53 | },
|
54 | 54 | };
|
55 | 55 | },
|
56 | 56 | };
|
57 |
| - } |
| 57 | + }; |
58 | 58 |
|
59 | 59 | fieldMiddleware(): FieldMiddleware {
|
60 | 60 | return ({ info, args }, next) => {
|
|
0 commit comments