-
-
Notifications
You must be signed in to change notification settings - Fork 77
feat(cloudflare): Support wrapping workers main file #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -76,4 +76,12 @@ describe('cloudflare-worker', () => { | |
| '"binding": "CF_VERSION_METADATA"', | ||
| ]); | ||
| }); | ||
|
|
||
| it('modifies the worker file to include Sentry initialization', () => { | ||
| checkFileContents(`${projectDir}/src/index.ts`, [ | ||
| 'import * as Sentry from "@sentry/cloudflare";', | ||
| 'export default Sentry.withSentry(env => ({', | ||
| 'dsn: "https://[email protected]/1337",', | ||
| ]); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,76 @@ | ||
| // @ts-expect-error - clack is ESM and TS complains about that. It works though | ||
| import clack from '@clack/prompts'; | ||
| import chalk from 'chalk'; | ||
| import { getCloudflareWorkerTemplate } from './templates'; | ||
| import fs from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { | ||
| getCloudflareWorkerTemplate, | ||
| getCloudflareWorkerTemplateWithHandler, | ||
| } from './templates'; | ||
| import { | ||
| defaultEntryPoint, | ||
| getEntryPointFromWranglerConfig, | ||
| } from './wrangler/get-entry-point-from-wrangler-config'; | ||
| import { wrapWorkerWithSentry } from './wrap-worker'; | ||
|
|
||
| /** | ||
| * Prints the Sentry worker template to the console. | ||
| * Currently focused on Cloudflare Workers, but the structure can be | ||
| * extended for other Cloudflare products in the future. | ||
| * Creates or updates the main worker file with Sentry initialization. | ||
| * Currently focused on Cloudflare Workers | ||
| */ | ||
| export function createSentryInitFile( | ||
| export async function createSentryInitFile( | ||
| dsn: string, | ||
| selectedFeatures: { | ||
| performance: boolean; | ||
| }, | ||
| ): void { | ||
| clack.log.step('Please wrap your handler with Sentry initialization:'); | ||
| ): Promise<void> { | ||
| const entryPointFromConfig = getEntryPointFromWranglerConfig(); | ||
|
|
||
| // eslint-disable-next-line no-console | ||
| console.log(chalk.cyan(getCloudflareWorkerTemplate(dsn, selectedFeatures))); | ||
| if (!entryPointFromConfig) { | ||
| clack.log.info( | ||
| 'No entry point found in wrangler config, creating a new one.', | ||
| ); | ||
|
|
||
| const cloudflareWorkerTemplate = getCloudflareWorkerTemplateWithHandler(); | ||
|
|
||
| await fs.promises.mkdir( | ||
| path.join(process.cwd(), path.dirname(defaultEntryPoint)), | ||
| { | ||
| recursive: true, | ||
| }, | ||
| ); | ||
| await fs.promises.writeFile( | ||
| path.join(process.cwd(), defaultEntryPoint), | ||
| cloudflareWorkerTemplate, | ||
| { encoding: 'utf-8', flag: 'w' }, | ||
| ); | ||
|
|
||
| clack.log.success(`Created ${chalk.cyan(defaultEntryPoint)}.`); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| const entryPointPath = path.join(process.cwd(), entryPointFromConfig); | ||
|
|
||
| if (fs.existsSync(entryPointPath)) { | ||
| clack.log.info( | ||
|
Comment on lines
+52
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: For new projects, the wizard creates a 🔍 Detailed AnalysisIn a fresh project without a 💡 Suggested FixThe 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||
| `Found existing entry point: ${chalk.cyan(entryPointFromConfig)}`, | ||
| ); | ||
|
|
||
| try { | ||
| await wrapWorkerWithSentry(entryPointPath, dsn, selectedFeatures); | ||
| clack.log.success( | ||
| `Wrapped ${chalk.cyan( | ||
| entryPointFromConfig, | ||
| )} with Sentry initialization.`, | ||
| ); | ||
| } catch (error) { | ||
| clack.log.warn('Failed to wrap worker automatically.'); | ||
| clack.log.step('Please wrap your handler with Sentry initialization:'); | ||
|
|
||
| clack.note( | ||
| chalk.cyan(getCloudflareWorkerTemplate(dsn, selectedFeatures)), | ||
| ); | ||
| } | ||
| return; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import * as recast from 'recast'; | ||
| import type { namedTypes as t } from 'ast-types'; | ||
| // @ts-expect-error - magicast is ESM and TS complains about that. It works though | ||
| import { loadFile, writeFile } from 'magicast'; | ||
| // @ts-expect-error - clack is ESM and TS complains about that. It works though | ||
| import * as clack from '@clack/prompts'; | ||
| import { hasSentryContent } from '../utils/ast-utils'; | ||
| import chalk from 'chalk'; | ||
| import { ExpressionKind } from 'ast-types/lib/gen/kinds'; | ||
|
|
||
| const b = recast.types.builders; | ||
|
|
||
| /** | ||
| * Wraps a Cloudflare Worker's default export with Sentry.withSentry() | ||
| * | ||
| * Before: | ||
| * ``` | ||
| * export default { | ||
| * async fetch(request, env, ctx) { ... } | ||
| * } satisfies ExportedHandler<Env>; | ||
| * ``` | ||
| * | ||
| * After: | ||
| * ``` | ||
| * import * as Sentry from '@sentry/cloudflare'; | ||
| * | ||
| * export default Sentry.withSentry( | ||
| * (env) => ({ | ||
| * dsn: 'your-dsn', | ||
| * tracesSampleRate: 1, | ||
| * }), | ||
| * { | ||
| * async fetch(request, env, ctx) { ... } | ||
| * } satisfies ExportedHandler<Env> | ||
| * ); | ||
| * ``` | ||
| * | ||
| * @param workerFilePath - Path to the worker file to wrap | ||
| * @param dsn - Sentry DSN for initialization | ||
| * @param selectedFeatures - Feature flags for optional Sentry features | ||
| */ | ||
| export async function wrapWorkerWithSentry( | ||
| workerFilePath: string, | ||
| dsn: string, | ||
| selectedFeatures: { | ||
| performance: boolean; | ||
| }, | ||
| ): Promise<void> { | ||
| const workerAst = await loadFile(workerFilePath); | ||
|
|
||
| if (hasSentryContent(workerAst.$ast as t.Program)) { | ||
| clack.log.warn( | ||
| `Sentry is already configured in ${chalk.cyan( | ||
| workerFilePath, | ||
| )}. Skipping wrapping with Sentry.`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| workerAst.imports.$add({ | ||
| from: '@sentry/cloudflare', | ||
| imported: '*', | ||
| local: 'Sentry', | ||
| }); | ||
|
|
||
| recast.visit(workerAst.$ast, { | ||
| visitExportDefaultDeclaration(path) { | ||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access | ||
| const originalDeclaration = path.value.declaration as ExpressionKind; | ||
| const sentryConfig = createSentryConfigFunction(dsn, selectedFeatures); | ||
| const wrappedExport = b.callExpression( | ||
| b.memberExpression(b.identifier('Sentry'), b.identifier('withSentry')), | ||
| [sentryConfig, originalDeclaration], | ||
| ); | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
| path.value.declaration = wrappedExport; | ||
|
|
||
| return false; | ||
| }, | ||
| }); | ||
|
|
||
| await writeFile(workerAst.$ast, workerFilePath); | ||
| } | ||
|
|
||
| /** | ||
| * Creates the Sentry config function: (env) => ({ dsn: '...', ... }) | ||
| */ | ||
| function createSentryConfigFunction( | ||
| dsn: string, | ||
| selectedFeatures: { | ||
| performance: boolean; | ||
| }, | ||
| ): t.ArrowFunctionExpression { | ||
| const configProperties: t.ObjectProperty[] = [ | ||
| b.objectProperty(b.identifier('dsn'), b.stringLiteral(dsn)), | ||
| ]; | ||
|
|
||
| if (selectedFeatures.performance) { | ||
| const tracesSampleRateProperty = b.objectProperty( | ||
| b.identifier('tracesSampleRate'), | ||
| b.numericLiteral(1), | ||
| ); | ||
|
|
||
| tracesSampleRateProperty.comments = [ | ||
| b.commentLine( | ||
| ' Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.', | ||
| true, | ||
| false, | ||
| ), | ||
| ]; | ||
|
|
||
| configProperties.push(tracesSampleRateProperty); | ||
| } | ||
|
|
||
| const configObject = b.objectExpression(configProperties); | ||
|
|
||
| return b.arrowFunctionExpression([b.identifier('env')], configObject); | ||
| } |
This comment was marked as outdated.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.