Skip to content
This repository has been archived by the owner on Jul 2, 2024. It is now read-only.

Commit

Permalink
DEVPROD-356: Add Sentry grouping (#491)
Browse files Browse the repository at this point in the history
  • Loading branch information
sophstad authored Feb 22, 2024
1 parent 3fb6e18 commit 375015c
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 30 deletions.
37 changes: 30 additions & 7 deletions src/components/ErrorHandling/Sentry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import {
captureException,
getClient,
init,
setTags,
withScope,
} from "@sentry/react";
import type { Scope, SeverityLevel } from "@sentry/react";
import type { Context, Primitive } from "@sentry/types";
import {
getReleaseStage,
getSentryDSN,
Expand All @@ -28,20 +30,41 @@ const initializeSentry = () => {

const isInitialized = () => !!getClient();

const sendError = (
err: Error,
severity: SeverityLevel,
metadata?: { [key: string]: any },
) => {
export type ErrorInput = {
err: Error;
fingerprint?: string[];
context?: Context;
severity: SeverityLevel;
tags?: { [key: string]: Primitive };
};

const sendError = ({
context,
err,
fingerprint,
severity,
tags,
}: ErrorInput) => {
withScope((scope) => {
setScope(scope, { context: metadata, level: severity });
setScope(scope, { context, level: severity });

if (fingerprint) {
// A custom fingerprint allows for more intelligent grouping
scope.setFingerprint(fingerprint);
}

if (tags) {
// Apply tags, which are a searchable/filterable property
setTags(tags);
}

captureException(err);
});
};

type ScopeOptions = {
level?: SeverityLevel;
context?: { [key: string]: any };
context?: Context;
};

const setScope = (scope: Scope, { context, level }: ScopeOptions = {}) => {
Expand Down
13 changes: 10 additions & 3 deletions src/gql/GQLProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ import { reportError } from "utils/errorReporting";
const logErrorsLink = onError(({ graphQLErrors, operation }) => {
if (Array.isArray(graphQLErrors)) {
graphQLErrors.forEach((gqlErr) => {
const fingerprint = [operation.operationName];
if (gqlErr?.path?.length) {
fingerprint.push(...gqlErr.path);
}
reportError(new Error(gqlErr.message), {
gqlErr,
operationName: operation.operationName,
variables: operation.variables,
context: {
gqlErr,
variables: operation.variables,
},
fingerprint,
tags: { operationName: operation.operationName },
}).warning();
});
}
Expand Down
1 change: 1 addition & 0 deletions src/gql/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2402,6 +2402,7 @@ export type SpruceConfig = {
jira?: Maybe<JiraConfig>;
keys: Array<SshKey>;
providers?: Maybe<CloudProviderConfig>;
secretFields: Array<Scalars["String"]["output"]>;
slack?: Maybe<SlackConfig>;
spawnHost: SpawnHostConfig;
ui?: Maybe<UiConfig>;
Expand Down
10 changes: 4 additions & 6 deletions src/hooks/useLogDownloader/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,11 @@ const useLogDownloader = ({
downloadSizeLimit,
onIncompleteDownload: (reason, incompleteDownloadError) => {
reportError(new Error("Incomplete download"), {
message: reason,
metadata: {
context: {
incompleteDownloadError,
message: reason,
url,
},
name: "Log download incomplete",
}).warning();

dispatchToast.warning(LOG_FILE_DOWNLOAD_TOO_LARGE_WARNING, true, {
Expand Down Expand Up @@ -99,12 +98,11 @@ const useLogDownloader = ({
reason: "Log line size limit exceeded",
});
reportError(new Error("Incomplete download"), {
message: "Log line size limit exceeded",
metadata: {
context: {
message: "Log line size limit exceeded",
trimmedLines,
url,
},
name: "Log download incomplete",
}).warning();
dispatchToast.warning(LOG_LINE_TOO_LARGE_WARNING, true, {
shouldTimeout: false,
Expand Down
25 changes: 22 additions & 3 deletions src/utils/errorReporting/errorReporting.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,40 @@ describe("error reporting", () => {
expect(Sentry.captureException).toHaveBeenCalledWith(err);
});

it("supports metadata field", () => {
it("supports context field", () => {
mockEnv("NODE_ENV", "production");
jest.spyOn(Sentry, "captureException").mockImplementation(jest.fn());
const err = {
message: "GraphQL Error",
name: "Error Name",
};

const metadata = { customField: "foo" };
const result = reportError(err, metadata);
const context = { anything: "foo" };
const result = reportError(err, { context });
result.severe();
expect(Sentry.captureException).toHaveBeenCalledWith(err);
result.warning();
expect(Sentry.captureException).toHaveBeenCalledWith(err);
});

it("supports tags", () => {
mockEnv("NODE_ENV", "production");
jest.spyOn(Sentry, "captureException").mockImplementation(jest.fn());
jest.spyOn(Sentry, "setTags").mockImplementation(jest.fn());
const err = {
message: "GraphQL Error",
name: "Error Name",
};

const tags = { spruce: "true" };
const result = reportError(err, { tags });
result.severe();
expect(Sentry.captureException).toHaveBeenCalledWith(err);
expect(Sentry.setTags).toHaveBeenCalledWith(tags);
result.warning();
expect(Sentry.captureException).toHaveBeenCalledWith(err);
expect(Sentry.setTags).toHaveBeenCalledWith(tags);
});
});

describe("breadcrumbs", () => {
Expand Down
33 changes: 27 additions & 6 deletions src/utils/errorReporting/index.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,54 @@
import { Breadcrumb, addBreadcrumb } from "@sentry/react";
import { sendError as sentrySendError } from "components/ErrorHandling/Sentry";
import {
ErrorInput,
sendError as sentrySendError,
} from "components/ErrorHandling/Sentry";
import { isProductionBuild } from "utils/environmentVariables";

interface reportErrorResult {
severe: () => void;
warning: () => void;
}

type ErrorMetadata = {
fingerprint?: ErrorInput["fingerprint"];
tags?: ErrorInput["tags"];
context?: ErrorInput["context"];
};

const reportError = (
err: Error,
metadata?: { [key: string]: any },
{ context, fingerprint, tags }: ErrorMetadata = {},
): reportErrorResult => {
if (!isProductionBuild()) {
return {
severe: () => {
console.error({ err, metadata, severity: "severe" });
console.error({ context, err, fingerprint, severity: "severe", tags });
},
warning: () => {
console.error({ err, metadata, severity: "warning" });
console.error({ context, err, fingerprint, severity: "warning", tags });
},
};
}

return {
severe: () => {
sentrySendError(err, "error", metadata);
sentrySendError({
context,
err,
fingerprint,
severity: "error",
tags,
});
},
warning: () => {
sentrySendError(err, "warning", metadata);
sentrySendError({
context,
err,
fingerprint,
severity: "warning",
tags,
});
},
};
};
Expand Down
10 changes: 5 additions & 5 deletions src/utils/matchingLines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@ export const constructRegexToMatch = (visibleFilters: Filters) => {
});
} catch (e) {
// If we get an error here, it means the regex is invalid and got past the validation step. We should report this error.
reportError(new Error("Invalid regex for filter"), {
message: `The regex "${f.expression}" is invalid`,
metadata: e,
name: "Invalid Regex",
}).severe();
if (e instanceof Error) {
reportError(new Error("Invalid regex for filter"), {
context: { error: e.toString(), expression: f.expression },
}).severe();
}
}
}
});
Expand Down

0 comments on commit 375015c

Please sign in to comment.