-
Notifications
You must be signed in to change notification settings - Fork 751
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Send Crash Reports to Sentry (#4571)
* Initial sentry implementation * Prompt every time * Add tests * Create pink-bags-push.md * Clear event queue * Make event queue non-const
- Loading branch information
Showing
13 changed files
with
346 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"wrangler": minor | ||
--- | ||
|
||
feat: When Wrangler crashes, send an error report to Sentry to aid in debugging. | ||
|
||
When Wrangler's top-level exception handler catches an error thrown from Wrangler's application, it will offer to report the error to Sentry. This requires opt-in from the user every time. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -68,6 +68,7 @@ jobs: | |
NODE_ENV: "production" | ||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} | ||
ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} | ||
SENTRY_DSN: "https://[email protected]/583" | ||
CI_OS: ${{ runner.os }} | ||
|
||
- name: Pack miniflare | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,7 @@ jobs: | |
# this is the "test/staging" key for sparrow analytics | ||
SPARROW_SOURCE_KEY: "5adf183f94b3436ba78d67f506965998" | ||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} | ||
SENTRY_DSN: "https://[email protected]/583" | ||
ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} | ||
working-directory: packages/wrangler | ||
|
||
|
@@ -111,6 +112,7 @@ jobs: | |
NODE_ENV: "production" | ||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} | ||
ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} | ||
SENTRY_DSN: "https://[email protected]/583" | ||
CI_OS: ${{ runner.os }} | ||
|
||
- name: Build & Publish Prerelease Registry | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,8 @@ jobs: | |
NPM_PUBLISH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} | ||
ALGOLIA_APP_ID: ${{ secrets.ALGOLIA_APP_ID }} | ||
ALGOLIA_PUBLIC_KEY: ${{ secrets.ALGOLIA_PUBLIC_KEY }} | ||
SENTRY_DSN: "https://[email protected]/583" | ||
|
||
NODE_ENV: "production" | ||
# This is the "production" key for sparrow analytics. | ||
# Include this here because this step will rebuild Wrangler and needs to have this available | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { rest } from "msw"; | ||
|
||
import { mockAccountId, mockApiToken } from "./helpers/mock-account-id"; | ||
import { mockConsoleMethods } from "./helpers/mock-console"; | ||
import { clearDialogs, mockConfirm } from "./helpers/mock-dialogs"; | ||
import { useMockIsTTY } from "./helpers/mock-istty"; | ||
import { msw } from "./helpers/msw"; | ||
import { runInTempDir } from "./helpers/run-in-tmp"; | ||
import { runWrangler } from "./helpers/run-wrangler"; | ||
|
||
declare const global: { SENTRY_DSN: string | undefined }; | ||
|
||
describe("sentry", () => { | ||
const ORIGINAL_SENTRY_DSN = global.SENTRY_DSN; | ||
const std = mockConsoleMethods(); | ||
runInTempDir(); | ||
mockAccountId(); | ||
mockApiToken(); | ||
const { setIsTTY } = useMockIsTTY(); | ||
|
||
let sentryRequests: { count: number } | undefined; | ||
|
||
beforeEach(() => { | ||
global.SENTRY_DSN = | ||
"https://[email protected]/24601"; | ||
|
||
sentryRequests = mockSentryEndpoint(); | ||
}); | ||
afterEach(() => { | ||
global.SENTRY_DSN = ORIGINAL_SENTRY_DSN; | ||
clearDialogs(); | ||
msw.resetHandlers(); | ||
}); | ||
describe("non interactive", () => { | ||
it("should not hit sentry in normal usage", async () => { | ||
await runWrangler("version"); | ||
expect(sentryRequests?.count).toEqual(0); | ||
}); | ||
|
||
it("should not hit sentry after error", async () => { | ||
await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( | ||
`[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` | ||
); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
" | ||
[32mIf you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose[0m | ||
? Would you like to report this error to Cloudflare? | ||
🤖 Using fallback value in non-interactive context: no" | ||
`); | ||
expect(sentryRequests?.count).toEqual(0); | ||
}); | ||
}); | ||
describe("interactive", () => { | ||
beforeEach(() => { | ||
setIsTTY(true); | ||
}); | ||
afterEach(() => { | ||
setIsTTY(false); | ||
}); | ||
it("should not hit sentry in normal usage", async () => { | ||
await runWrangler("version"); | ||
expect(sentryRequests?.count).toEqual(0); | ||
}); | ||
it("should not hit sentry after error when permission denied", async () => { | ||
mockConfirm({ | ||
text: "Would you like to report this error to Cloudflare?", | ||
result: false, | ||
}); | ||
await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( | ||
`[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` | ||
); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
" | ||
[32mIf you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose[0m" | ||
`); | ||
expect(sentryRequests?.count).toEqual(0); | ||
}); | ||
it("should hit sentry after error when permission provided", async () => { | ||
mockConfirm({ | ||
text: "Would you like to report this error to Cloudflare?", | ||
result: true, | ||
}); | ||
await expect(runWrangler("delete")).rejects.toMatchInlineSnapshot( | ||
`[AssertionError: A worker name must be defined, either via --name, or in wrangler.toml]` | ||
); | ||
expect(std.out).toMatchInlineSnapshot(` | ||
" | ||
[32mIf you think this is a bug then please create an issue at https://github.com/cloudflare/workers-sdk/issues/new/choose[0m" | ||
`); | ||
// Sentry sends multiple HTTP requests to capture breadcrumbs | ||
expect(sentryRequests?.count).toBeGreaterThan(0); | ||
}); | ||
}); | ||
}); | ||
|
||
function mockSentryEndpoint() { | ||
const requests = { count: 0 }; | ||
msw.use( | ||
rest.post( | ||
`https://platform.dash.cloudflare.com/sentry/envelope`, | ||
async (req, res, cxt) => { | ||
requests.count++; | ||
return res(cxt.status(200), cxt.json({})); | ||
} | ||
) | ||
); | ||
|
||
return requests; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import * as Sentry from "@sentry/node"; | ||
import { rejectedSyncPromise } from "@sentry/utils"; | ||
import { fetch } from "undici"; | ||
import { version as wranglerVersion } from "../../package.json"; | ||
import { confirm } from "../dialogs"; | ||
import { logger } from "../logger"; | ||
import type { BaseTransportOptions, TransportRequest } from "@sentry/types"; | ||
import type { RequestInit } from "undici"; | ||
|
||
let sentryReportingAllowed = false; | ||
|
||
// The SENTRY_DSN is provided at esbuild time as a `define` for production and beta releases. | ||
// Otherwise it is left undefined, which disables reporting. | ||
declare const SENTRY_DSN: string; | ||
|
||
/* Returns a Sentry transport for the Sentry proxy Worker. */ | ||
export const makeSentry10Transport = (options: BaseTransportOptions) => { | ||
let eventQueue: [string, RequestInit][] = []; | ||
|
||
const transportSentry10 = async (request: TransportRequest) => { | ||
/* Adds helpful properties to the request body before we send it to our | ||
proxy Worker. These properties can be parsed out from the NDJSON in | ||
`request.body`, but it's easier and safer to just attach them here. */ | ||
const sentryWorkerPayload = { | ||
envelope: request.body, | ||
url: options.url, | ||
}; | ||
|
||
try { | ||
if (sentryReportingAllowed) { | ||
const eventsToSend = [...eventQueue]; | ||
eventQueue = []; | ||
for (const event of eventsToSend) { | ||
await fetch(event[0], event[1]); | ||
} | ||
|
||
const response = await fetch( | ||
`https://platform.dash.cloudflare.com/sentry/envelope`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
Accept: "*/*", | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(sentryWorkerPayload), | ||
} | ||
); | ||
|
||
return { | ||
statusCode: response.status, | ||
headers: { | ||
"x-sentry-rate-limits": response.headers.get( | ||
"X-Sentry-Rate-Limits" | ||
), | ||
"retry-after": response.headers.get("Retry-After"), | ||
}, | ||
}; | ||
} else { | ||
// We don't currently have permission to send this event, but maybe we will in the future. | ||
// Add to an in-memory just in case | ||
eventQueue.push([ | ||
`https://platform.dash.cloudflare.com/sentry/envelope`, | ||
{ | ||
method: "POST", | ||
headers: { | ||
Accept: "*/*", | ||
"Content-Type": "application/json", | ||
}, | ||
body: JSON.stringify(sentryWorkerPayload), | ||
}, | ||
]); | ||
return { | ||
statusCode: 200, | ||
}; | ||
} | ||
} catch (err) { | ||
console.log(err); | ||
|
||
return rejectedSyncPromise(err); | ||
} | ||
}; | ||
|
||
return Sentry.createTransport(options, transportSentry10); | ||
}; | ||
|
||
export function setupSentry() { | ||
if (typeof SENTRY_DSN !== "undefined") { | ||
Sentry.init({ | ||
release: `wrangler@${wranglerVersion}`, | ||
dsn: SENTRY_DSN, | ||
transport: makeSentry10Transport, | ||
}); | ||
} | ||
} | ||
|
||
export function addBreadcrumb( | ||
message: string, | ||
level: Sentry.SeverityLevel = "log" | ||
) { | ||
if (typeof SENTRY_DSN !== "undefined") { | ||
Sentry.addBreadcrumb({ | ||
message, | ||
level, | ||
}); | ||
} | ||
} | ||
|
||
// Capture top-level Wrangler errors. Also take this opportunity to ask the user for | ||
// consent if not already granted. | ||
export async function captureGlobalException(e: unknown) { | ||
if (typeof SENTRY_DSN !== "undefined") { | ||
sentryReportingAllowed = await confirm( | ||
"Would you like to report this error to Cloudflare?", | ||
{ fallbackValue: false } | ||
); | ||
|
||
if (!sentryReportingAllowed) { | ||
logger.debug(`Sentry: Reporting disabled - would have sent ${e}.`); | ||
return; | ||
} | ||
|
||
logger.debug(`Sentry: Capturing exception ${e}`); | ||
Sentry.captureException(e); | ||
} | ||
} | ||
|
||
// Ensure we send Sentry events before Wrangler exits | ||
export async function closeSentry() { | ||
if (typeof SENTRY_DSN !== "undefined") { | ||
await Sentry.close(); | ||
} | ||
} |
Oops, something went wrong.