Skip to content

Instantly share code, notes, and snippets.

@andreialecu
Last active April 6, 2023 17:44
Show Gist options
  • Save andreialecu/40cb13c01b0d88c8163bbec3de59c580 to your computer and use it in GitHub Desktop.
Save andreialecu/40cb13c01b0d88c8163bbec3de59c580 to your computer and use it in GitHub Desktop.
Sentry NestJS GQL instrumentation

Ref: getsentry/sentry-javascript#4071 (comment)

Used more or less like:

    {
      provide: APP_INTERCEPTOR,
      useClass: TracingInterceptor,
    },
    { 
      // optional
      provide: SENTRY_USER_PROVIDER,
      useClass: AppSentryUserProvider
    },
mongoose.plugin(mongooseProfilerPlugin);
import { SentryUserProvider } from '...';
import { Injectable } from '@nestjs/common';
import { User } from '@sentry/types';
import requestIp from 'request-ip';
import { UserModel } from '...';
import { UsersService } from '...';
@Injectable()
export class AppSentryUserProvider implements SentryUserProvider {
constructor(private userService: UsersService) {}
async getSentryUser(
gqlContext: any,
): Promise<{ user: User; tags: Record<string, string> | undefined }> {
const req = gqlContext.req;
let user: UserModel | undefined;
if (req?.headers['authorization']) {
const token = req.headers['authorization'].replace('Bearer', '').trim();
if (token) {
user = await this.userService.findUser(token);
}
}
const ip = req?.headers ? requestIp.getClientIp(req) : undefined;
const groupId = user?.groupId;
const tags = groupId
? {
userGroupId: groupId,
}
: undefined;
const sentryUser: User = {
id: user?._id,
username: user?.username,
ip_address: ip,
};
return { tags, user: sentryUser };
}
}
import { Transaction } from '@sentry/types';
import { AsyncLocalStorage } from 'async_hooks';
const storage = new AsyncLocalStorage<Transaction>();
export const getCurrentTransaction = (): Transaction | undefined =>
storage.getStore();
export const runWithSentry = storage.run.bind(storage);
import { Span } from "@sentry/types";
import { mongoose } from "@typegoose/typegoose";
import { getCurrentTransaction } from "./hub";
type WithSpan = {
_span?: Span;
};
export function mongooseProfilerPlugin(schema: mongoose.Schema): void {
// pre find
schema.pre(
/find(.*)/,
function preFind(
this: mongoose.Query<unknown, unknown> & WithSpan & { op: string }
) {
try {
const transaction = getCurrentTransaction();
const collectionName = this.model.collection.name;
const op = this.op;
if (transaction) {
this._span = transaction.startChild({
op: "db",
description: [op, collectionName].join("."),
tags: {
method: op,
collection: collectionName,
},
data: {
conditions: this.getFilter(),
},
});
}
} catch (err) {
console.error(err);
}
}
);
// pre save
schema.pre("save", function preSave() {
try {
const transaction = getCurrentTransaction();
const collectionName = this.collection?.name;
const op = "save";
if (transaction && collectionName) {
(this as WithSpan)._span = transaction.startChild({
op: "db",
description: [op, collectionName].join("."),
tags: {
method: op,
collection: collectionName,
id: "" + this._id,
},
});
}
} catch (err) {
console.error(err);
}
});
// post find
schema.post(/find(.*)/, function postFind(doc: unknown) {
try {
const span = (this as WithSpan)._span;
if (typeof doc === "object") {
span?.setData(
"resultCount",
Array.isArray(doc) ? doc.length : doc ? 1 : 0
);
}
span?.finish();
(this as WithSpan)._span = undefined;
} catch (err) {
console.error(err);
}
});
// post save
schema.post(
"save",
function postSave(this: mongoose.Document & { _span: Span | undefined }) {
try {
this._span?.finish();
this._span = undefined;
} catch (err) {
console.error(err);
}
}
);
}
import * as Sentry from "@sentry/node";
export const SENTRY_USER_PROVIDER = "SENTRY_USER_PROVIDER";
export interface SentryUserProvider {
getSentryUser(gqlContext: any): Promise<{
user: Sentry.User;
tags: Record<string, string> | undefined;
}>;
}
import {
CallHandler,
ExecutionContext,
Inject,
Injectable,
NestInterceptor,
} from "@nestjs/common";
import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql";
import { InjectSentry, SentryService } from "nestjs-sentry";
import * as Sentry from "@sentry/node";
import { SpanStatus } from "@sentry/tracing";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { runWithSentry } from "./hub";
import {
SentryUserProvider,
SENTRY_USER_PROVIDER,
} from "./sentry-user.provider";
@Injectable()
export class TracingInterceptor implements NestInterceptor {
constructor(
@InjectSentry() private sentry: SentryService,
@Inject(SENTRY_USER_PROVIDER)
private sentryUserProvider: SentryUserProvider
) {}
async intercept(
context: ExecutionContext,
next: CallHandler
): Promise<Observable<unknown>> {
if (context.getType<GqlContextType>() !== "graphql") {
// only handle graphql requests
return next.handle();
}
const client = this.sentry.instance().getCurrentHub().getClient();
const hub = new Sentry.Hub(client);
const gqlContext = GqlExecutionContext.create(context).getContext();
const { user, tags } = await this.sentryUserProvider.getSentryUser(
gqlContext
);
const req = gqlContext.req;
const transaction = hub.startTransaction({
op: "resolver",
name: [context.getClass().name, context.getHandler().name].join("."),
tags: {
class: context.getClass().name,
handler: context.getHandler().name,
...tags,
},
});
hub.configureScope((scope) => {
scope.setSpan(transaction);
scope.setUser(user);
});
return new Observable((observer) => {
runWithSentry(transaction, () => {
next
.handle()
.pipe(
tap({
complete: () => {
transaction.finish();
},
error: (exception) => {
transaction.setStatus(SpanStatus.InternalError);
transaction.finish();
hub.withScope((scope) => {
if (req) {
const data = Sentry.Handlers.parseRequest({}, req);
scope.setExtra("req", data.request);
if (data.extra) scope.setExtras(data.extra);
}
scope.setUser(user);
hub.captureException(exception);
});
},
})
)
.subscribe({
next: (res) => observer.next(res),
error: (error) => observer.error(error),
complete: () => observer.complete(),
});
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment