8000 feat(tracing): Support Apollo/GraphQL with NestJS (#7194) · HPLai/sentry-javascript@a8449de · GitHub
  • [go: up one dir, main page]

    Skip to content

    Commit a8449de

    Browse files
    feat(tracing): Support Apollo/GraphQL with NestJS (getsentry#7194)
    Co-authored-by: Abhijeet Prasad <aprasad@sentry.io>
    1 parent 79babe9 commit a8449de

    File tree

    2 files changed

    +230
    -44
    lines changed

    2 files changed

    +230
    -44
    lines changed

    packages/tracing/src/integrations/node/apollo.ts

    Lines changed: 110 additions & 44 deletions
    Original file line numberDiff line numberDiff line change
    @@ -4,6 +4,10 @@ import { arrayify, fill, isThenable, loadModule, logger } from '@sentry/utils';
    44

    55
    import { shouldDisableAutoInstrumentation } from './utils/node-utils';
    66

    7+
    interface ApolloOptions {
    8+
    useNestjs?: boolean;
    9+
    }
    10+
    711
    type ApolloResolverGroup = {
    812
    [key: string]: () => unknown;
    913
    };
    @@ -24,6 +28,19 @@ export class Apollo implements Integration {
    2428
    */
    2529
    public name: string = Apollo.id;
    2630

    31+
    private readonly _useNest: boolean;
    32+
    33+
    /**
    34+
    * @inheritDoc
    35+
    */
    36+
    public constructor(
    37+
    options: ApolloOptions = {
    38+
    useNestjs: false,
    39+
    },
    40+
    ) {
    41+
    this._useNest = !!options.useNestjs;
    42+
    }
    43+
    2744
    /**
    2845
    * @inheritDoc
    2946
    */
    @@ -33,62 +50,111 @@ export class Apollo implements Integration {
    3350
    return;
    3451
    }
    3552

    36-
    const pkg = loadModule<{
    37-
    ApolloServerBase: {
    38-
    prototype: {
    39-
    constructSchema: () => unknown;
    53+
    if (this._useNest) {
    54+
    const pkg = loadModule<{
    55+
    GraphQLFactory: {
    56+
    prototype: {
    57+
    create: (resolvers: ApolloModelResolvers[]) => unknown;
    58+
    };
    4059
    };
    41-
    };
    42-
    }>('apollo-server-core');
    60+
    }>('@nestjs/graphql');
    4361

    44-
    if (!pkg) {
    45-
    __DEBUG_BUILD__ && logger.error('Apollo Integration was unable to require apollo-server-core package.');
    46-
    return;
    47-
    }
    62+
    if (!pkg) {
    63+
    __DEBUG_BUILD__ && logger.error('Apollo-NestJS Integration was unable to require @nestjs/graphql package.');
    64+
    return;
    65+
    }
    66+
    67+
    /**
    68+
    * Iterate over resolvers of NestJS ResolversExplorerService before schemas are constructed.
    69+
    */
    70+
    fill(
    71+
    pkg.GraphQLFactory.prototype,
    72+
    'mergeWithSchema',
    73+
    function (orig: (this: unknown, ...args: unknown[]) => unknown) {
    74+
    return function (
    75+
    this: { resolversExplorerService: { explore: () => ApolloModelResolvers[] } },
    76+
    ...args: unknown[]
    77+
    ) {
    78+
    fill(this.resolversExplorerService, 'explore', function (orig: () => ApolloModelResolvers[]) {
    79+
    return function (this: unknown) {
    80+
    const resolvers = arrayify(orig.call(this));
    81+
    82+
    const instrumentedResolvers = instrumentResolvers(resolvers, getCurrentHub);
    83+
    84+
    return instrumentedResolvers;
    85+
    };
    86+
    });
    87+
    88+
    return orig.call(this, ...args);
    89+
    };
    90+
    },
    91+
    );
    92+
    } else {
    93+
    const pkg = loadModule<{
    94+
    ApolloServerBase: {
    95+
    prototype: {
    96+
    constructSchema: (config: unknown) => unknown;
    97+
    };
    98+
    };
    99+
    }>('apollo-server-core');
    100+
    101+
    if (!pkg) {
    102+
    __DEBUG_BUILD__ && logger.error('Apollo Integration was unable to require apollo-server-core package.');
    103+
    return;
    104+
    }
    105+
    106+
    /**
    107+
    * Iterate over resolvers of the ApolloServer instance before schemas are constructed.
    108+
    */
    109+
    fill(pkg.ApolloServerBase.prototype, 'constructSchema', function (orig: (config: unknown) => unknown) {
    110+
    return function (this: {
    111+
    config: { resolvers?: ApolloModelResolvers[]; schema?: unknown; modules?: unknown };
    112+
    }) {
    113+
    if (!this.config.resolvers) {
    114+
    if (__DEBUG_BUILD__) {
    115+
    if (this.config.schema) {
    116+
    logger.warn(
    117+
    'Apollo integration is not able to trace `ApolloServer` instances constructed via `schema` property.' +
    118+
    'If you are using NestJS with Apollo, please use `Sentry.Integrations.Apollo({ useNestjs: true })` instead.',
    119+
    );
    120+
    logger.warn();
    121+
    } else if (this.config.modules) {
    122+
    logger.warn(
    123+
    'Apollo integration is not able to trace `ApolloServer` instances constructed via `modules` property.',
    124+
    );
    125+
    }
    48126

    49-
    /**
    50-
    * Iterate over resolvers of the ApolloServer instance before schemas are constructed.
    51-
    */
    52-
    fill(pkg.ApolloServerBase.prototype, 'constructSchema', function (orig: () => unknown) {
    53-
    return function (this: { config: { resolvers?: ApolloModelResolvers[]; schema?: unknown; modules?: unknown } }) {
    54-
    if (!this.config.resolvers) {
    55-
    if (__DEBUG_BUILD__) {
    56-
    if (this.config.schema) {
    57-
    logger.warn(
    58-
    'Apollo integration is not able to trace `ApolloServer` instances constructed via `schema` property.',
    59-
    );
    60-
    } else if (this.config.modules) {
    61-
    logger.warn(
    62-
    'Apollo integration is not able to trace `ApolloServer` instances constructed via `modules` property.',
    63-
    );
    127+
    logger.error('Skipping tracing as no resolvers found on the `ApolloServer` instance.');
    64128
    }
    65129

    66-
    logger.error('Skipping tracing as no resolvers found on the `ApolloServer` instance.');
    130+
    return orig.call(this);
    67131
    }
    68132

    69-
    return orig.call(this);
    70-
    }
    133+
    const resolvers = arrayify(this.config.resolvers);
    71134

    72-
    const resolvers = arrayify(this.config.resolvers);
    73-
    74-
    this.config.resolvers = resolvers.map(model => {
    75-
    Object.keys(model).forEach(resolverGroupName => {
    76-
    Object.keys(model[resolverGroupName]).forEach(resolverName => {
    77-
    if (typeof model[resolverGroupName][resolverName] !== 'function') {
    78-
    return;
    79-
    }
    135+
    this.config.resolvers = instrumentResolvers(resolvers, getCurrentHub);
    80136

    81-
    wrapResolver(model, resolverGroupName, resolverName, getCurrentHub);
    82-
    });
    83-
    });
    137+
    return orig.call(this);
    138+
    };
    139+
    });
    140+
    }
    141+
    }
    142+
    }
    84143

    85-
    return model;
    86-
    });
    144+
    function instrumentResolvers(resolvers: ApolloModelResolvers[], getCurrentHub: () => Hub): ApolloModelResolvers[] {
    145+
    return resolvers.map(model => {
    146+
    Object.keys(model).forEach(resolverGroupName => {
    147+
    Object.keys(model[resolverGroupName]).forEach(resolverName => {
    148+
    if (typeof model[resolverGroupName][resolverName] !== 'function') {
    149+
    return;
    150+
    }
    87151

    88-
    return orig.call(this);
    89-
    };
    152+
    wrapResolver(model, resolverGroupName, resolverName, getCurrentHub);
    153+
    });
    90154
    });
    91-
    }
    155+
    156+
    return model;
    157+
    });
    92158
    }
    93159

    94160
    /**
    Lines changed: 120 additions & 0 deletions
    Original file line numberDiff line numberDiff line change
    @@ -0,0 +1,120 @@
    1+
    /* eslint-disable @typescript-eslint/unbound-method */
    2+
    import { Hub, Scope } from '@sentry/core';
    3+
    import { logger } from '@sentry/utils';
    4+
    5+
    import { Apollo } from '../../src/integrations/node/apollo';
    6+
    import { Span } from '../../src/span';
    7+
    import { getTestClient } from '../testutils';
    8+
    9+
    type ApolloResolverGroup = {
    10+
    [key: string]: () => unknown;
    11+
    };
    12+
    13+
    type ApolloModelResolvers = {
    14+
    [key: string]: ApolloResolverGroup;
    15+
    };
    16+
    17+
    class GraphQLFactory {
    18+
    _resolvers: ApolloModelResolvers[];
    19+
    resolversExplorerService = {
    20+
    explore: () => this._resolvers,
    21+
    };
    22+
    constructor() {
    23+
    this._resolvers = [
    24+
    {
    25+
    Query: {
    26+
    res_1(..._args: unknown[]) {
    27+
    return 'foo';
    28+
    },
    29+
    },
    30+
    Mutation: {
    31+
    res_2(..._args: unknown[]) {
    32+
    return 'bar';
    33+
    },
    34+
    },
    35+
    },
    36+
    ];
    37+
    38+
    this.mergeWithSchema();
    39+
    }
    40+
    41+
    public mergeWithSchema(..._args: unknown[]) {
    42+
    return this.resolversExplorerService.explore();
    43+
    }
    44+
    }
    45+
    46+
    // mock for @nestjs/graphql package
    47+
    jest.mock('@sentry/utils', () => {
    48+
    const actual = jest.requireActual('@sentry/utils');
    49+
    return {
    50+
    ...actual,
    51+
    loadModule() {
    52+
    return {
    53+
    GraphQLFactory,
    54+
    };
    55+
    },
    56+
    };
    57+
    });
    58+
    59+
    describe('setupOnce', () => {
    60+
    let scope = new Scope();
    61+
    let parentSpan: Span;
    62+
    let childSpan: Span;
    63+
    let GraphQLFactoryInstance: GraphQLFactory;
    64+
    65+
    beforeAll(() => {
    66+
    new Apollo({
    67+
    useNestjs: true,
    68+
    }).setupOnce(
    69+
    () => undefined,
    70+
    () => new Hub(undefined, scope),
    71+
    );
    72+
    73+
    GraphQLFactoryInstance = new GraphQLFactory();
    74+
    });
    75+
    76+
    beforeEach(() => {
    77+
    scope = new Scope();
    78+
    parentSpan = new Span();
    79+
    childSpan = parentSpan.startChild();
    80+
    jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan);
    81+
    jest.spyOn(scope, 'setSpan');
    82+
    jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan);
    83+
    jest.spyOn(childSpan, 'finish');
    84+
    });
    85+
    86+
    it('should wrap a simple resolver', () => {
    87+
    GraphQLFactoryInstance._resolvers[0]?.['Query']?.['res_1']?.();
    88+
    expect(scope.getSpan).toBeCalled();
    89+
    expect(parentSpan.startChild).toBeCalledWith({
    90+
    description: 'Query.res_1',
    91+
    op: 'graphql.resolve',
    92+
    });
    93+
    expect(childSpan.finish).toBeCalled();
    94+
    });
    95+
    96+
    it('should wrap another simple resolver', () => {
    97+
    GraphQLFactoryInstance._resolvers[0]?.['Mutation']?.['res_2']?.();
    98+
    expect(scope.getSpan).toBeCalled();
    99+
    expect(parentSpan.startChild).toBeCalledWith({
    100+
    description: 'Mutation.res_2',
    101+
    op: 'graphql.resolve',
    102+
    });
    103+
    expect(childSpan.finish).toBeCalled();
    104+
    });
    105+
    106+
    it("doesn't attach when using otel instrumenter", () => {
    107+
    const loggerLogSpy = jest.spyOn(logger, 'log');
    108+
    109+
    const client = getTestClient({ instrumenter: 'otel' });
    110+
    const hub = new Hub(client);
    111+
    112+
    const integration = new Apollo({ useNestjs: true });
    113+
    integration.setupOnce(
    114+
    () => {},
    115+
    () => hub,
    116+
    );
    117+
    118+
    expect(loggerLogSpy).toBeCalledWith('Apollo Integration is skipped because of instrumenter configuration.');
    119+
    });
    120+
    });

    0 commit comments

    Comments
     (0)
    0