Skip to content

feat(astro): Add sentryAstro integration #9218

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

Merged
merged 10 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/astro/.eslintrc.js → packages/astro/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,12 @@ module.exports = {
project: ['tsconfig.test.json'],
},
},
{
files: ['src/integration/**', 'src/server/**'],
rules: {
'@sentry-internal/sdk/no-optional-chaining': 'off',
'@sentry-internal/sdk/no-nullish-coalescing': 'off',
},
},
],
};
10 changes: 10 additions & 0 deletions packages/astro/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# The paths in this file are specified so that they align with the file structure in `./build` after this file is copied
# into it by the prepack script `scripts/prepack.ts`.

*

!/cjs/**/*
!/esm/**/*
!/types/**/*
!/types-ts3.8/**/*
!/integration/**/*
96 changes: 95 additions & 1 deletion packages/astro/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,98 @@ This package is a wrapper around `@sentry/node` for the server and `@sentry/brow

## Installation and Setup

TODO
### 1. Registering the Sentry Astro integration:

Add the `sentryAstro` integration to your `astro.config.mjs` file:

```javascript
import { sentryAstro } from "@sentry/astro/integration";

export default defineConfig({
// Rest of your Astro project config
integrations: [
sentryAstro({
dsn: '__DSN__',
}),
],
})
```

This is the easiest way to configure Sentry in an Astro project.
You can pass a few additional options to `sentryAstro` but the SDK comes preconfigured in an opinionated way.
If you want to fully customize your SDK setup, you can do so, too:

### 2. [Optional] Uploading Source Maps

To upload source maps to Sentry, simply add the `project` and `authToken` options to `sentryAstro`:

```js
// astro.config.mjs
import { sentryAstro } from "@sentry/astro/integration";

export default defineConfig({
// Rest of your Astro project config
integrations: [
sentryAstro({
dsn: '__DSN__',
project: 'your-project-slug',
authToken: import.meta.env('SENTRY_AUTH_TOKEN'),
}),
],
})
```

You can also define these values as environment variables in e.g. a `.env` file
or in you CI configuration:

```sh
SENTRY_PROJECT="your-project"
SENTRY_AUTH_TOKEN="your-token"
```

Follow [this guide](https://docs.sentry.io/product/accounts/auth-tokens/#organization-auth-tokens) to create an auth token.

### 3. [Optional] Advanced Configuration

To fully customize and configure Sentry in an Astro project, follow step 1 and in addition,
add a `sentry.client.config.(js|ts)` and `sentry.server.config(js|ts)` file to the root directory of your project.
Inside these files, you can call `Sentry.init()` and use the full range of Sentry options.

Configuring the client SDK:

```js
// sentry.client.config.ts or sentry.server.config.ts
import * as Sentry from "@sentry/astro";

Sentry.init({
dsn: "__DSN__",
beforeSend(event) {
console.log("Sending event on the client");
return event;
},
tracesSampler: () => {/* ... */}
});
```

**Important**: Once you created a sentry config file, the SDK options passed to `sentryAstro` will be ignored for the respective runtime. You can also only define create of the two files.

#### 3.1 Custom file location

If you want to move the `sentry.*.config` files to another location,
you can specify the file path, relative to the project root, in `sentryAstro`:

```js
// astro.config.mjs
import { sentryAstro } from "@sentry/astro/integration";

export default defineConfig({
// Rest of your Astro project config
integrations: [
sentryAstro({
dsn: '__DSN__',
clientInitPath: '.config/sentry.client.init.js',
serverInitPath: '.config/sentry.server.init.js',
}),
],
})
```
17 changes: 14 additions & 3 deletions packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,33 @@
"engines": {
"node": ">=18.14.1"
},
"main": "build/cjs/index.server.js",
"type": "module",
"main": "build/cjs/index.client.js",
"module": "build/esm/index.server.js",
"browser": "build/esm/index.client.js",
"types": "build/types/index.types.d.ts",
"exports": {
".": {
"node": "./build/esm/index.server.js",
"browser": "./build/esm/index.client.js",
"import": "./build/esm/index.client.js",
"require": "./build/cjs/index.server.js",
"types": "./build/types/index.types.d.ts"
}
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"astro": "1.x"
"astro": "3.x"
},
"dependencies": {
"@sentry/browser": "7.73.0",
"@sentry/node": "7.73.0",
"@sentry/core": "7.73.0",
"@sentry/utils": "7.73.0",
"@sentry/types": "7.73.0"
"@sentry/types": "7.73.0",
"@sentry/vite-plugin": "^2.8.0"
},
"devDependencies": {
"astro": "^3.2.3",
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/rollup.npm.config.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js';

export default makeNPMConfigVariants(
const variants = makeNPMConfigVariants(
makeBaseNPMConfig({
entrypoints: ['src/index.server.ts', 'src/index.client.ts'],
packageSpecificConfig: {
output: {
dynamicImportInCjs: true,
exports: 'named',
},
},
// Astro is Node 18+ no need to add polyfills
addPolyfills: false,
}),
);

export default variants;
20 changes: 20 additions & 0 deletions packages/astro/scripts/syncIntegration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable no-console */

import * as fse from 'fs-extra';
import * as path from 'path';

const buildDir = path.resolve('build');
const srcIntegrationDir = path.resolve(path.join('src', 'integration'));
const destIntegrationDir = path.resolve(path.join(buildDir, 'integration'));

try {
fse.copySync(srcIntegrationDir, destIntegrationDir, {
filter: (src, _) => {
return !src.endsWith('.md');
},
});
console.log('\nCopied Astro integration to ./build/integration\n');
} catch (e) {
console.error('\nError while copying integration to build dir:');
console.error(e);
}
5 changes: 5 additions & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
// Unfortunately, we cannot `export * from '@sentry/node'` because in prod builds,
// Vite puts these exports into a `default` property (Sentry.default) rather than
// on the top - level namespace.

import { sentryAstro } from './integration';

// Hence, we export everything from the Node SDK explicitly:
export {
addGlobalEventProcessor,
Expand Down Expand Up @@ -58,3 +61,5 @@ export {
export * from '@sentry/node';

export { init } from './server/sdk';

export default sentryAstro;
81 changes: 81 additions & 0 deletions packages/astro/src/integration/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/* eslint-disable no-console */
import { sentryVitePlugin } from '@sentry/vite-plugin';
import type { AstroIntegration } from 'astro';
import * as fs from 'fs';
import * as path from 'path';

import { buildClientSnippet, buildSdkInitFileImportSnippet, buildServerSnippet } from './snippets';
import type { SentryOptions } from './types';

const PKG_NAME = '@sentry/astro';

export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => {
return {
name: PKG_NAME,
hooks: {
'astro:config:setup': async ({ updateConfig, injectScript }) => {
// The third param here enables loading of all env vars, regardless of prefix
// see: https://main.vitejs.dev/config/#using-environment-variables-in-config

// TODO: Ideally, we want to load the environment with vite like this:
// const env = loadEnv('production', process.cwd(), '');
// However, this currently throws a build error.
// Will revisit this later.
const env = process.env;

const uploadOptions = options.sourceMapsUploadOptions || {};

const shouldUploadSourcemaps = uploadOptions?.enabled ?? true;
const authToken = uploadOptions.authToken || env.SENTRY_AUTH_TOKEN;

if (shouldUploadSourcemaps && authToken) {
updateConfig({
vite: {
build: {
sourcemap: true,
},
plugins: [
sentryVitePlugin({
org: uploadOptions.org ?? env.SENTRY_ORG,
project: uploadOptions.project ?? env.SENTRY_PROJECT,
authToken: uploadOptions.authToken ?? env.SENTRY_AUTH_TOKEN,
telemetry: uploadOptions.telemetry ?? true,
}),
],
},
});
}

const pathToClientInit = options.clientInitPath
? path.resolve(options.clientInitPath)
: findDefaultSdkInitFile('client');
const pathToServerInit = options.serverInitPath
? path.resolve(options.serverInitPath)
: findDefaultSdkInitFile('server');

if (pathToClientInit) {
options.debug && console.log(`[sentry-astro] Using ${pathToClientInit} for client init.`);
injectScript('page', buildSdkInitFileImportSnippet(pathToClientInit));
} else {
options.debug && console.log('[sentry-astro] Using default client init.');
injectScript('page', buildClientSnippet(options || {}));
}

if (pathToServerInit) {
options.debug && console.log(`[sentry-astro] Using ${pathToServerInit} for server init.`);
injectScript('page-ssr', buildSdkInitFileImportSnippet(pathToServerInit));
} else {
options.debug && console.log('[sentry-astro] Using default server init.');
injectScript('page-ssr', buildServerSnippet(options || {}));
}
},
},
};
};

function findDefaultSdkInitFile(type: 'server' | 'client'): string | undefined {
const fileExtensions = ['ts', 'js', 'tsx', 'jsx', 'mjs', 'cjs', 'mts'];
return fileExtensions
.map(ext => path.resolve(path.join(process.cwd(), `sentry.${type}.config.${ext}`)))
.find(filename => fs.existsSync(filename));
}
45 changes: 45 additions & 0 deletions packages/astro/src/integration/snippets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { SentryOptions } from './types';

/**
* Creates a snippet that imports a Sentry.init file.
*/
export function buildSdkInitFileImportSnippet(filePath: string): string {
return `import "${filePath}";`;
}

/**
* Creates a snippet that initializes Sentry on the client by choosing
* default options.
*/
export function buildClientSnippet(options: SentryOptions): string {
return `import * as Sentry from "@sentry/astro";

Sentry.init({
${buildCommonInitOptions(options)}
integrations: [new Sentry.BrowserTracing(), new Sentry.Replay()],
replaysSessionSampleRate: ${options.replaysSessionSampleRate ?? 0.1},
replaysOnErrorSampleRate: ${options.replaysOnErrorSampleRate ?? 1.0},
});`;
}

/**
* Creates a snippet that initializes Sentry on the server by choosing
* default options.
*/
export function buildServerSnippet(options: SentryOptions): string {
return `import * as Sentry from "@sentry/astro";

Sentry.init({
${buildCommonInitOptions(options)}
});`;
}

const buildCommonInitOptions = (options: SentryOptions): string => `dsn: ${
options.dsn ? JSON.stringify(options.dsn) : 'import.meta.env.PUBLIC_SENTRY_DSN'
},
debug: ${options.debug ? true : false},
environment: ${options.environment ? JSON.stringify(options.environment) : 'import.meta.env.PUBLIC_VERCEL_ENV'},
release: ${options.release ? JSON.stringify(options.release) : 'import.meta.env.PUBLIC_VERCEL_GIT_COMMIT_SHA'},
tracesSampleRate: ${options.tracesSampleRate ?? 1.0},${
options.sampleRate ? `\n sampleRate: ${options.sampleRate},` : ''
}`;
Loading