diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9f1440515c4f..3191f92adefb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -379,7 +379,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v3
with:
- node-version: ${{ env.DEFAULT_NODE_VERSION }}
+ node-version-file: 'package.json'
- name: Restore caches
uses: ./.github/actions/restore-cache
env:
@@ -406,7 +406,7 @@ jobs:
- name: Set up Node
uses: actions/setup-node@v3
with:
- node-version: ${{ matrix.node }}
+ node-version-file: 'package.json'
- name: Set up Bun
uses: oven-sh/setup-bun@v1
- name: Restore caches
@@ -419,6 +419,38 @@ jobs:
- name: Compute test coverage
uses: codecov/codecov-action@v3
+ job_deno_unit_tests:
+ name: Deno Unit Tests
+ needs: [job_get_metadata, job_build]
+ timeout-minutes: 10
+ runs-on: ubuntu-20.04
+ strategy:
+ fail-fast: false
+ steps:
+ - name: Check out current commit (${{ needs.job_get_metadata.outputs.commit_label }})
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ env.HEAD_COMMIT }}
+ - name: Set up Node
+ uses: actions/setup-node@v3
+ with:
+ node-version-file: 'package.json'
+ - name: Set up Deno
+ uses: denoland/setup-deno@v1.1.3
+ with:
+ deno-version: v1.37.1
+ - name: Restore caches
+ uses: ./.github/actions/restore-cache
+ env:
+ DEPENDENCY_CACHE_KEY: ${{ needs.job_build.outputs.dependency_cache_key }}
+ - name: Run tests
+ run: |
+ cd packages/deno
+ yarn build
+ yarn test
+ - name: Compute test coverage
+ uses: codecov/codecov-action@v3
+
job_node_unit_tests:
name: Node (${{ matrix.node }}) Unit Tests
needs: [job_get_metadata, job_build]
@@ -895,6 +927,7 @@ jobs:
job_browser_build_tests,
job_browser_unit_tests,
job_bun_unit_tests,
+ job_deno_unit_tests,
job_node_unit_tests,
job_nextjs_integration_test,
job_node_integration_tests,
diff --git a/.gitignore b/.gitignore
index 777b23658572..d6eee47e4eed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,8 @@ jest/transformers/*.js
# node tarballs
packages/*/sentry-*.tgz
.nxcache
+# The Deno types are downloaded before building
+packages/deno/lib.deno.d.ts
# logs
yarn-error.log
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index da74f03528af..3ad96b1733d5 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -4,6 +4,7 @@
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
- "augustocdias.tasks-shell-input"
- ],
+ "augustocdias.tasks-shell-input",
+ "denoland.vscode-deno"
+ ]
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d3c8a08448c6..96bd2dfb42b9 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -28,5 +28,6 @@
{
"mode": "auto"
}
- ]
+ ],
+ "deno.enablePaths": ["packages/deno/test"]
}
diff --git a/package.json b/package.json
index 9546ebf2f9df..b5cbf9ddc38e 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,7 @@
"postpublish": "lerna run --stream --concurrency 1 postpublish",
"test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test",
"test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\" test:unit",
- "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,node,node-experimental,opentelemetry-node,serverless,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"",
+ "test-ci-browser": "lerna run test --ignore \"@sentry/{bun,deno,node,node-experimental,opentelemetry-node,serverless,nextjs,remix,gatsby,sveltekit,vercel-edge}\" --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,overhead-metrics}\"",
"test-ci-node": "ts-node ./scripts/node-unit-tests.ts",
"test-ci-bun": "lerna run test --scope @sentry/bun",
"test:update-snapshots": "lerna run test:update-snapshots",
@@ -45,6 +45,7 @@
"packages/browser-integration-tests",
"packages/bun",
"packages/core",
+ "packages/deno",
"packages/e2e-tests",
"packages/ember",
"packages/eslint-config-sdk",
diff --git a/packages/deno/.eslintrc.js b/packages/deno/.eslintrc.js
new file mode 100644
index 000000000000..b92652708339
--- /dev/null
+++ b/packages/deno/.eslintrc.js
@@ -0,0 +1,18 @@
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ ignorePatterns: ['lib.deno.d.ts', 'scripts/*.mjs'],
+ rules: {
+ '@sentry-internal/sdk/no-optional-chaining': 'off',
+ '@sentry-internal/sdk/no-nullish-coalescing': 'off',
+ '@sentry-internal/sdk/no-unsupported-es6-methods': 'off',
+ '@sentry-internal/sdk/no-class-field-initializers': 'off',
+ },
+ overrides: [
+ {
+ files: ['./test/*.ts'],
+ rules: {
+ 'import/no-unresolved': 'off',
+ },
+ },
+ ],
+};
diff --git a/packages/deno/LICENSE b/packages/deno/LICENSE
new file mode 100644
index 000000000000..d11896ba1181
--- /dev/null
+++ b/packages/deno/LICENSE
@@ -0,0 +1,14 @@
+Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
+persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
+Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/deno/README.md b/packages/deno/README.md
new file mode 100644
index 000000000000..5be987a249af
--- /dev/null
+++ b/packages/deno/README.md
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+# Official Sentry SDK for Deno (Beta)
+
+[](https://www.npmjs.com/package/@sentry/deno)
+[](https://www.npmjs.com/package/@sentry/deno)
+[](https://www.npmjs.com/package/@sentry/deno)
+
+## Links
+
+- [Official SDK Docs](https://docs.sentry.io/quickstart/)
+- [TypeDoc](http://getsentry.github.io/sentry-javascript/)
+
+The Sentry Deno SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues).
+
+## Usage
+
+To use this SDK, call `Sentry.init(options)` as early as possible in the main entry module. This will initialize the SDK and
+hook into the environment. Note that you can turn off almost all side effects using the respective options.
+
+```javascript
+import * as Sentry from 'npm:@sentry/deno';
+
+Sentry.init({
+ dsn: '__DSN__',
+ // ...
+});
+```
+
+To set context information or send manual events, use the exported functions of `@sentry/deno`. Note that these
+functions will not perform any action before you have called `init()`:
+
+```javascript
+// Set user information, as well as tags and further extras
+Sentry.configureScope(scope => {
+ scope.setExtra('battery', 0.7);
+ scope.setTag('user_mode', 'admin');
+ scope.setUser({ id: '4711' });
+ // scope.clear();
+});
+
+// Add a breadcrumb for future events
+Sentry.addBreadcrumb({
+ message: 'My Breadcrumb',
+ // ...
+});
+
+// Capture exceptions, messages or manual events
+Sentry.captureMessage('Hello, world!');
+Sentry.captureException(new Error('Good bye'));
+Sentry.captureEvent({
+ message: 'Manual',
+ stacktrace: [
+ // ...
+ ],
+});
+```
+
+
+
diff --git a/packages/deno/jest.config.js b/packages/deno/jest.config.js
new file mode 100644
index 000000000000..24f49ab59a4c
--- /dev/null
+++ b/packages/deno/jest.config.js
@@ -0,0 +1 @@
+module.exports = require('../../jest/jest.config.js');
diff --git a/packages/deno/package.json b/packages/deno/package.json
new file mode 100644
index 000000000000..7ee06d1874dc
--- /dev/null
+++ b/packages/deno/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "@sentry/deno",
+ "version": "7.73.0",
+ "description": "Official Sentry SDK for Deno",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno",
+ "author": "Sentry",
+ "license": "MIT",
+ "main": "build/index.js",
+ "module": "build/index.js",
+ "types": "build/index.d.ts",
+ "private": true,
+ "dependencies": {
+ "@sentry/core": "7.73.0",
+ "@sentry/browser": "7.73.0",
+ "@sentry/types": "7.73.0",
+ "@sentry/utils": "7.73.0",
+ "lru_map": "^0.3.3"
+ },
+ "devDependencies": {
+ "@types/node": "20.8.2",
+ "@rollup/plugin-commonjs": "^25.0.5",
+ "@rollup/plugin-typescript": "^11.1.5",
+ "rollup-plugin-dts": "^6.1.0"
+ },
+ "scripts": {
+ "deno-types": "node ./scripts/download-deno-types.mjs",
+ "build": "run-s build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "yarn deno-types && rollup -c rollup.config.js",
+ "build:types": "run-s deno-types build:types:tsc build:types:bundle",
+ "build:types:tsc": "tsc -p tsconfig.types.json",
+ "build:types:bundle": "rollup -c rollup.types.config.js",
+ "circularDepCheck": "madge --circular src/index.ts",
+ "clean": "rimraf build coverage",
+ "prefix": "yarn deno-types",
+ "fix": "run-s fix:eslint fix:prettier",
+ "fix:eslint": "eslint . --format stylish --fix",
+ "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"",
+ "prelint": "yarn deno-types",
+ "lint": "run-s lint:prettier lint:eslint",
+ "lint:eslint": "eslint . --format stylish",
+ "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
+ "install:deno": "node ./scripts/install-deno.mjs",
+ "test": "run-s deno-types install:deno test:types test:unit",
+ "test:types": "deno check ./build/index.js",
+ "test:unit": "deno test --allow-read --allow-run",
+ "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false,
+ "madge": {
+ "detectiveOptions": {
+ "ts": {
+ "skipTypeImports": true
+ }
+ }
+ }
+}
diff --git a/packages/deno/rollup.config.js b/packages/deno/rollup.config.js
new file mode 100644
index 000000000000..48123037a596
--- /dev/null
+++ b/packages/deno/rollup.config.js
@@ -0,0 +1,24 @@
+import nodeResolve from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+import sucrase from '@rollup/plugin-sucrase';
+
+export default {
+ input: ['src/index.ts'],
+ output: {
+ dir: 'build',
+ sourcemap: true,
+ preserveModules: false,
+ strict: false,
+ freeze: false,
+ interop: 'auto',
+ format: 'esm',
+ banner: '/// ',
+ },
+ plugins: [
+ nodeResolve({
+ extensions: ['.mjs', '.js', '.json', '.node', '.ts', '.tsx'],
+ }),
+ commonjs(),
+ sucrase({ transforms: ['typescript'] }),
+ ],
+};
diff --git a/packages/deno/rollup.types.config.js b/packages/deno/rollup.types.config.js
new file mode 100644
index 000000000000..d8123b6c5cd3
--- /dev/null
+++ b/packages/deno/rollup.types.config.js
@@ -0,0 +1,17 @@
+import dts from 'rollup-plugin-dts';
+
+export default {
+ input: './build/index.d.ts',
+ output: [{ file: 'build/index.d.ts', format: 'es' }],
+ plugins: [
+ dts({ respectExternal: true }),
+ // The bundled types contain a declaration for the __DEBUG_BUILD__ global
+ // This can result in errors about duplicate global declarations so we strip it out!
+ {
+ name: 'strip-global',
+ renderChunk(code) {
+ return { code: code.replace(/declare global \{\s*const __DEBUG_BUILD__: boolean;\s*\}/g, '') };
+ },
+ },
+ ],
+};
diff --git a/packages/deno/scripts/download-deno-types.mjs b/packages/deno/scripts/download-deno-types.mjs
new file mode 100644
index 000000000000..33bdfcf5ebb7
--- /dev/null
+++ b/packages/deno/scripts/download-deno-types.mjs
@@ -0,0 +1,7 @@
+import { writeFileSync, existsSync } from 'fs';
+import { download } from './download.mjs';
+
+if (!existsSync('lib.deno.d.ts')) {
+ const code = await download('https://github.com/denoland/deno/releases/download/v1.37.1/lib.deno.d.ts');
+ writeFileSync('lib.deno.d.ts', code);
+}
diff --git a/packages/deno/scripts/download.mjs b/packages/deno/scripts/download.mjs
new file mode 100644
index 000000000000..25bcc39f583a
--- /dev/null
+++ b/packages/deno/scripts/download.mjs
@@ -0,0 +1,10 @@
+/** Download a url to a string */
+export async function download(url) {
+ try {
+ return await fetch(url).then(res => res.text());
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Failed to download', url, e);
+ process.exit(1);
+ }
+}
diff --git a/packages/deno/scripts/install-deno.mjs b/packages/deno/scripts/install-deno.mjs
new file mode 100644
index 000000000000..aa7235a278ff
--- /dev/null
+++ b/packages/deno/scripts/install-deno.mjs
@@ -0,0 +1,26 @@
+import { execSync } from 'child_process';
+
+import { download } from './download.mjs';
+
+try {
+ execSync('deno --version', { stdio: 'inherit' });
+} catch (_) {
+ // eslint-disable-next-line no-console
+ console.error('Deno is not installed. Installing...');
+ if (process.platform === 'win32') {
+ // TODO
+ // eslint-disable-next-line no-console
+ console.error('Please install Deno manually: https://docs.deno.com/runtime/manual/getting_started/installation');
+ process.exit(1);
+ } else {
+ const script = await download('https://deno.land/x/install/install.sh');
+
+ try {
+ execSync(script, { stdio: 'inherit' });
+ } catch (e) {
+ // eslint-disable-next-line no-console
+ console.error('Failed to install Deno', e);
+ process.exit(1);
+ }
+ }
+}
diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts
new file mode 100644
index 000000000000..3eb7db655428
--- /dev/null
+++ b/packages/deno/src/client.ts
@@ -0,0 +1,44 @@
+import type { ServerRuntimeClientOptions } from '@sentry/core';
+import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core';
+
+import type { DenoClientOptions } from './types';
+
+function getHostName(): string | undefined {
+ const result = Deno.permissions.querySync({ name: 'sys', kind: 'hostname' });
+ return result.state === 'granted' ? Deno.hostname() : undefined;
+}
+
+/**
+ * The Sentry Deno SDK Client.
+ *
+ * @see DenoClientOptions for documentation on configuration options.
+ * @see SentryClient for usage documentation.
+ */
+export class DenoClient extends ServerRuntimeClient {
+ /**
+ * Creates a new Deno SDK instance.
+ * @param options Configuration options for this SDK.
+ */
+ public constructor(options: DenoClientOptions) {
+ options._metadata = options._metadata || {};
+ options._metadata.sdk = options._metadata.sdk || {
+ name: 'sentry.javascript.deno',
+ packages: [
+ {
+ name: 'denoland:sentry',
+ version: SDK_VERSION,
+ },
+ ],
+ version: SDK_VERSION,
+ };
+
+ const clientOptions: ServerRuntimeClientOptions = {
+ ...options,
+ platform: 'deno',
+ runtime: { name: 'deno', version: Deno.version.deno },
+ serverName: options.serverName || getHostName(),
+ };
+
+ super(clientOptions);
+ }
+}
diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts
new file mode 100644
index 000000000000..6a92adea2513
--- /dev/null
+++ b/packages/deno/src/index.ts
@@ -0,0 +1,77 @@
+export type {
+ Breadcrumb,
+ BreadcrumbHint,
+ PolymorphicRequest,
+ Request,
+ SdkInfo,
+ Event,
+ EventHint,
+ Exception,
+ Session,
+ // eslint-disable-next-line deprecation/deprecation
+ Severity,
+ SeverityLevel,
+ Span,
+ StackFrame,
+ Stacktrace,
+ Thread,
+ Transaction,
+ User,
+} from '@sentry/types';
+export type { AddRequestDataToEventOptions } from '@sentry/utils';
+
+export type { DenoOptions } from './types';
+
+export {
+ addGlobalEventProcessor,
+ addBreadcrumb,
+ captureException,
+ captureEvent,
+ captureMessage,
+ close,
+ configureScope,
+ createTransport,
+ extractTraceparentData,
+ flush,
+ getActiveTransaction,
+ getHubFromCarrier,
+ getCurrentHub,
+ Hub,
+ lastEventId,
+ makeMain,
+ runWithAsyncContext,
+ Scope,
+ startTransaction,
+ SDK_VERSION,
+ setContext,
+ setExtra,
+ setExtras,
+ setTag,
+ setTags,
+ setUser,
+ spanStatusfromHttpCode,
+ trace,
+ withScope,
+ captureCheckIn,
+ setMeasurement,
+ getActiveSpan,
+ startSpan,
+ startInactiveSpan,
+ startSpanManual,
+} from '@sentry/core';
+export type { SpanStatusType } from '@sentry/core';
+
+export { DenoClient } from './client';
+
+export { defaultIntegrations, init } from './sdk';
+
+import { Integrations as CoreIntegrations } from '@sentry/core';
+
+import * as DenoIntegrations from './integrations';
+
+const INTEGRATIONS = {
+ ...CoreIntegrations,
+ ...DenoIntegrations,
+};
+
+export { INTEGRATIONS as Integrations };
diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts
new file mode 100644
index 000000000000..49269c81be4e
--- /dev/null
+++ b/packages/deno/src/integrations/context.ts
@@ -0,0 +1,64 @@
+import type { Event, EventProcessor, Integration } from '@sentry/types';
+
+function getOSName(): string {
+ switch (Deno.build.os) {
+ case 'darwin':
+ return 'macOS';
+ case 'linux':
+ return 'Linux';
+ case 'windows':
+ return 'Windows';
+ default:
+ return Deno.build.os;
+ }
+}
+
+function getOSRelease(): string | undefined {
+ return Deno.permissions.querySync({ name: 'sys', kind: 'osRelease' }).state === 'granted'
+ ? Deno.osRelease()
+ : undefined;
+}
+
+async function denoRuntime(event: Event): Promise {
+ event.contexts = {
+ ...{
+ app: {
+ app_start_time: new Date(Date.now() - performance.now()).toISOString(),
+ },
+ device: {
+ arch: Deno.build.arch,
+ // eslint-disable-next-line no-restricted-globals
+ processor_count: navigator.hardwareConcurrency,
+ },
+ os: {
+ name: getOSName(),
+ version: getOSRelease(),
+ },
+ v8: {
+ name: 'v8',
+ version: Deno.version.v8,
+ },
+ typescript: {
+ name: 'TypeScript',
+ version: Deno.version.typescript,
+ },
+ },
+ ...event.contexts,
+ };
+
+ return event;
+}
+
+/** Adds Electron context to events. */
+export class DenoContext implements Integration {
+ /** @inheritDoc */
+ public static id = 'DenoContext';
+
+ /** @inheritDoc */
+ public name: string = DenoContext.id;
+
+ /** @inheritDoc */
+ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
+ addGlobalEventProcessor(async (event: Event) => denoRuntime(event));
+ }
+}
diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts
new file mode 100644
index 000000000000..47cd3a09218d
--- /dev/null
+++ b/packages/deno/src/integrations/contextlines.ts
@@ -0,0 +1,116 @@
+import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types';
+import { addContextToFrame } from '@sentry/utils';
+import { LRUMap } from 'lru_map';
+
+const FILE_CONTENT_CACHE = new LRUMap(100);
+const DEFAULT_LINES_OF_CONTEXT = 7;
+
+/**
+ * Resets the file cache. Exists for testing purposes.
+ * @hidden
+ */
+export function resetFileContentCache(): void {
+ FILE_CONTENT_CACHE.clear();
+}
+
+/**
+ * Reads file contents and caches them in a global LRU cache.
+ *
+ * @param filename filepath to read content from.
+ */
+async function readSourceFile(filename: string): Promise {
+ const cachedFile = FILE_CONTENT_CACHE.get(filename);
+ // We have a cache hit
+ if (cachedFile !== undefined) {
+ return cachedFile;
+ }
+
+ let content: string | null = null;
+ try {
+ content = await Deno.readTextFile(filename);
+ } catch (_) {
+ //
+ }
+
+ FILE_CONTENT_CACHE.set(filename, content);
+ return content;
+}
+
+interface ContextLinesOptions {
+ /**
+ * Sets the number of context lines for each frame when loading a file.
+ * Defaults to 7.
+ *
+ * Set to 0 to disable loading and inclusion of source files.
+ */
+ frameContextLines?: number;
+}
+
+/** Add node modules / packages to the event */
+export class ContextLines implements Integration {
+ /**
+ * @inheritDoc
+ */
+ public static id = 'ContextLines';
+
+ /**
+ * @inheritDoc
+ */
+ public name: string = ContextLines.id;
+
+ public constructor(private readonly _options: ContextLinesOptions = {}) {}
+
+ /** Get's the number of context lines to add */
+ private get _contextLines(): number {
+ return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
+ addGlobalEventProcessor(event => this.addSourceContext(event));
+ }
+
+ /** Processes an event and adds context lines */
+ public async addSourceContext(event: Event): Promise {
+ if (this._contextLines > 0 && event.exception && event.exception.values) {
+ for (const exception of event.exception.values) {
+ if (exception.stacktrace && exception.stacktrace.frames) {
+ await this.addSourceContextToFrames(exception.stacktrace.frames);
+ }
+ }
+ }
+
+ return event;
+ }
+
+ /** Adds context lines to frames */
+ public async addSourceContextToFrames(frames: StackFrame[]): Promise {
+ const contextLines = this._contextLines;
+
+ for (const frame of frames) {
+ // Only add context if we have a filename and it hasn't already been added
+ if (frame.filename && frame.in_app && frame.context_line === undefined) {
+ const permission = await Deno.permissions.query({
+ name: 'read',
+ path: frame.filename,
+ });
+
+ if (permission.state == 'granted') {
+ const sourceFile = await readSourceFile(frame.filename);
+
+ if (sourceFile) {
+ try {
+ const lines = sourceFile.split('\n');
+ addContextToFrame(lines, frame, contextLines);
+ } catch (_) {
+ // anomaly, being defensive in case
+ // unlikely to ever happen in practice but can definitely happen in theory
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts
new file mode 100644
index 000000000000..7e4d2e003673
--- /dev/null
+++ b/packages/deno/src/integrations/globalhandlers.ts
@@ -0,0 +1,165 @@
+import type { ServerRuntimeClient } from '@sentry/core';
+import { flush, getCurrentHub } from '@sentry/core';
+import type { Event, EventHint, Hub, Integration, Primitive, StackParser } from '@sentry/types';
+import { addExceptionMechanism, eventFromUnknownInput, isPrimitive } from '@sentry/utils';
+
+type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection';
+
+/** JSDoc */
+type GlobalHandlersIntegrations = Record;
+
+let isExiting = false;
+
+/** Global handlers */
+export class GlobalHandlers implements Integration {
+ /**
+ * @inheritDoc
+ */
+ public static id = 'GlobalHandlers';
+
+ /**
+ * @inheritDoc
+ */
+ public name: string = GlobalHandlers.id;
+
+ /** JSDoc */
+ private readonly _options: GlobalHandlersIntegrations;
+
+ /**
+ * Stores references functions to installing handlers. Will set to undefined
+ * after they have been run so that they are not used twice.
+ */
+ private _installFunc: Record void) | undefined> = {
+ error: installGlobalErrorHandler,
+ unhandledrejection: installGlobalUnhandledRejectionHandler,
+ };
+
+ /** JSDoc */
+ public constructor(options?: GlobalHandlersIntegrations) {
+ this._options = {
+ error: true,
+ unhandledrejection: true,
+ ...options,
+ };
+ }
+ /**
+ * @inheritDoc
+ */
+ public setupOnce(): void {
+ const options = this._options;
+
+ // We can disable guard-for-in as we construct the options object above + do checks against
+ // `this._installFunc` for the property.
+ // eslint-disable-next-line guard-for-in
+ for (const key in options) {
+ const installFunc = this._installFunc[key as GlobalHandlersIntegrationsOptionKeys];
+ if (installFunc && options[key as GlobalHandlersIntegrationsOptionKeys]) {
+ installFunc();
+ this._installFunc[key as GlobalHandlersIntegrationsOptionKeys] = undefined;
+ }
+ }
+ }
+}
+
+function installGlobalErrorHandler(): void {
+ globalThis.addEventListener('error', data => {
+ if (isExiting) {
+ return;
+ }
+
+ const [hub, stackParser] = getHubAndOptions();
+ const { message, error } = data;
+
+ const event = eventFromUnknownInput(getCurrentHub, stackParser, error || message);
+
+ event.level = 'fatal';
+
+ addMechanismAndCapture(hub, error, event, 'error');
+
+ // Stop the app from exiting for now
+ data.preventDefault();
+ isExiting = true;
+
+ void flush().then(() => {
+ // rethrow to replicate Deno default behavior
+ throw error;
+ });
+ });
+}
+
+function installGlobalUnhandledRejectionHandler(): void {
+ globalThis.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
+ if (isExiting) {
+ return;
+ }
+
+ const [hub, stackParser] = getHubAndOptions();
+ let error = e;
+
+ // dig the object of the rejection out of known event types
+ try {
+ if ('reason' in e) {
+ error = e.reason;
+ }
+ } catch (_oO) {
+ // no-empty
+ }
+
+ const event = isPrimitive(error)
+ ? eventFromRejectionWithPrimitive(error)
+ : eventFromUnknownInput(getCurrentHub, stackParser, error, undefined);
+
+ event.level = 'fatal';
+
+ addMechanismAndCapture(hub, error as unknown as Error, event, 'unhandledrejection');
+
+ // Stop the app from exiting for now
+ e.preventDefault();
+ isExiting = true;
+
+ void flush().then(() => {
+ // rethrow to replicate Deno default behavior
+ throw error;
+ });
+ });
+}
+
+/**
+ * Create an event from a promise rejection where the `reason` is a primitive.
+ *
+ * @param reason: The `reason` property of the promise rejection
+ * @returns An Event object with an appropriate `exception` value
+ */
+function eventFromRejectionWithPrimitive(reason: Primitive): Event {
+ return {
+ exception: {
+ values: [
+ {
+ type: 'UnhandledRejection',
+ // String() is needed because the Primitive type includes symbols (which can't be automatically stringified)
+ value: `Non-Error promise rejection captured with value: ${String(reason)}`,
+ },
+ ],
+ },
+ };
+}
+
+function addMechanismAndCapture(hub: Hub, error: EventHint['originalException'], event: Event, type: string): void {
+ addExceptionMechanism(event, {
+ handled: false,
+ type,
+ });
+ hub.captureEvent(event, {
+ originalException: error,
+ });
+}
+
+function getHubAndOptions(): [Hub, StackParser] {
+ const hub = getCurrentHub();
+ const client = hub.getClient();
+ const options = (client && client.getOptions()) || {
+ stackParser: () => [],
+ attachStacktrace: false,
+ };
+ return [hub, options.stackParser];
+}
diff --git a/packages/deno/src/integrations/index.ts b/packages/deno/src/integrations/index.ts
new file mode 100644
index 000000000000..97e439649bfc
--- /dev/null
+++ b/packages/deno/src/integrations/index.ts
@@ -0,0 +1,4 @@
+export { DenoContext } from './context';
+export { GlobalHandlers } from './globalhandlers';
+export { NormalizePaths } from './normalizepaths';
+export { ContextLines } from './contextlines';
diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts
new file mode 100644
index 000000000000..bf8a3986c93d
--- /dev/null
+++ b/packages/deno/src/integrations/normalizepaths.ts
@@ -0,0 +1,100 @@
+import type { Event, EventProcessor, Integration } from '@sentry/types';
+import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils';
+
+function appRootFromErrorStack(error: Error): string | undefined {
+ // We know at the other end of the stack from here is the entry point that called 'init'
+ // We assume that this stacktrace will traverse the root of the app
+ const frames = createStackParser(nodeStackLineParser())(error.stack || '');
+
+ const paths = frames
+ // We're only interested in frames that are in_app with filenames
+ .filter(f => f.in_app && f.filename)
+ .map(
+ f =>
+ (f.filename as string)
+ .replace(/^[A-Z]:/, '') // remove Windows-style prefix
+ .replace(/\\/g, '/') // replace all `\` instances with `/`
+ .split('/')
+ .filter(seg => seg !== ''), // remove empty segments
+ ) as string[][];
+
+ if (paths.length == 0) {
+ return undefined;
+ }
+
+ if (paths.length == 1) {
+ // Assume the single file is in the root
+ return dirname(paths[0].join('/'));
+ }
+
+ // Iterate over the paths and bail out when they no longer have a common root
+ let i = 0;
+ while (paths[0][i] && paths.every(w => w[i] === paths[0][i])) {
+ i++;
+ }
+
+ return paths[0].slice(0, i).join('/');
+}
+
+function getCwd(): string | undefined {
+ // We don't want to prompt for permissions so we only get the cwd if
+ // permissions are already granted
+ const permission = Deno.permissions.querySync({ name: 'read', path: './' });
+
+ try {
+ if (permission.state == 'granted') {
+ return Deno.cwd();
+ }
+ } catch (_) {
+ //
+ }
+
+ return undefined;
+}
+
+// Cached here
+let appRoot: string | undefined;
+
+function getAppRoot(error: Error): string | undefined {
+ if (appRoot === undefined) {
+ appRoot = getCwd() || appRootFromErrorStack(error);
+ }
+
+ return appRoot;
+}
+
+/** Normalises paths to the app root directory. */
+export class NormalizePaths implements Integration {
+ /** @inheritDoc */
+ public static id = 'NormalizePaths';
+
+ /** @inheritDoc */
+ public name: string = NormalizePaths.id;
+
+ /** @inheritDoc */
+ public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void {
+ // This error.stack hopefully contains paths that traverse the app cwd
+ const error = new Error();
+
+ addGlobalEventProcessor((event: Event): Event | null => {
+ const appRoot = getAppRoot(error);
+
+ if (appRoot) {
+ for (const exception of event.exception?.values || []) {
+ for (const frame of exception.stacktrace?.frames || []) {
+ if (frame.filename && frame.in_app) {
+ const startIndex = frame.filename.indexOf(appRoot);
+
+ if (startIndex > -1) {
+ const endIndex = startIndex + appRoot.length;
+ frame.filename = `app://${frame.filename.substring(endIndex)}`;
+ }
+ }
+ }
+ }
+ }
+
+ return event;
+ });
+ }
+}
diff --git a/packages/deno/src/sdk.ts b/packages/deno/src/sdk.ts
new file mode 100644
index 000000000000..cff16148453e
--- /dev/null
+++ b/packages/deno/src/sdk.ts
@@ -0,0 +1,102 @@
+import { Breadcrumbs, Dedupe, LinkedErrors } from '@sentry/browser';
+import type { ServerRuntimeClientOptions } from '@sentry/core';
+import { getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core';
+import type { StackParser } from '@sentry/types';
+import { createStackParser, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils';
+
+import { DenoClient } from './client';
+import { ContextLines, DenoContext, GlobalHandlers, NormalizePaths } from './integrations';
+import { makeFetchTransport } from './transports';
+import type { DenoOptions } from './types';
+
+export const defaultIntegrations = [
+ // Common
+ new CoreIntegrations.InboundFilters(),
+ new CoreIntegrations.FunctionToString(),
+ // From Browser
+ new Dedupe(),
+ new LinkedErrors(),
+ new Breadcrumbs({
+ dom: false,
+ history: false,
+ xhr: false,
+ }),
+ // Deno Specific
+ new DenoContext(),
+ new ContextLines(),
+ new NormalizePaths(),
+ new GlobalHandlers(),
+];
+
+const defaultStackParser: StackParser = createStackParser(nodeStackLineParser());
+
+/**
+ * The Sentry Deno SDK Client.
+ *
+ * To use this SDK, call the {@link init} function as early as possible in the
+ * main entry module. To set context information or send manual events, use the
+ * provided methods.
+ *
+ * @example
+ * ```
+ *
+ * import { init } from 'npm:@sentry/deno';
+ *
+ * init({
+ * dsn: '__DSN__',
+ * // ...
+ * });
+ * ```
+ *
+ * @example
+ * ```
+ *
+ * import { configureScope } from 'npm:@sentry/deno';
+ * configureScope((scope: Scope) => {
+ * scope.setExtra({ battery: 0.7 });
+ * scope.setTag({ user_mode: 'admin' });
+ * scope.setUser({ id: '4711' });
+ * });
+ * ```
+ *
+ * @example
+ * ```
+ *
+ * import { addBreadcrumb } from 'npm:@sentry/deno';
+ * addBreadcrumb({
+ * message: 'My Breadcrumb',
+ * // ...
+ * });
+ * ```
+ *
+ * @example
+ * ```
+ *
+ * import * as Sentry from 'npm:@sentry/deno';
+ * Sentry.captureMessage('Hello, world!');
+ * Sentry.captureException(new Error('Good bye'));
+ * Sentry.captureEvent({
+ * message: 'Manual',
+ * stacktrace: [
+ * // ...
+ * ],
+ * });
+ * ```
+ *
+ * @see {@link DenoOptions} for documentation on configuration options.
+ */
+export function init(options: DenoOptions = {}): void {
+ options.defaultIntegrations =
+ options.defaultIntegrations === false
+ ? []
+ : [...(Array.isArray(options.defaultIntegrations) ? options.defaultIntegrations : defaultIntegrations)];
+
+ const clientOptions: ServerRuntimeClientOptions = {
+ ...options,
+ stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser),
+ integrations: getIntegrationsToSetup(options),
+ transport: options.transport || makeFetchTransport,
+ };
+
+ initAndBind(DenoClient, clientOptions);
+}
diff --git a/packages/deno/src/transports/index.ts b/packages/deno/src/transports/index.ts
new file mode 100644
index 000000000000..62da327c5d83
--- /dev/null
+++ b/packages/deno/src/transports/index.ts
@@ -0,0 +1,46 @@
+import { createTransport } from '@sentry/core';
+import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types';
+import { rejectedSyncPromise } from '@sentry/utils';
+
+export interface DenoTransportOptions extends BaseTransportOptions {
+ /** Custom headers for the transport. Used by the XHRTransport and FetchTransport */
+ headers?: { [key: string]: string };
+}
+
+/**
+ * Creates a Transport that uses the Fetch API to send events to Sentry.
+ */
+export function makeFetchTransport(options: DenoTransportOptions): Transport {
+ const url = new URL(options.url);
+
+ if (Deno.permissions.querySync({ name: 'net', host: url.host }).state !== 'granted') {
+ // eslint-disable-next-line no-console
+ console.warn(`Sentry SDK requires 'net' permission to send events.
+Run with '--allow-net=${url.host}' to grant the requires permissions.`);
+ }
+
+ function makeRequest(request: TransportRequest): PromiseLike {
+ const requestOptions: RequestInit = {
+ body: request.body,
+ method: 'POST',
+ referrerPolicy: 'origin',
+ headers: options.headers,
+ };
+
+ try {
+ return fetch(options.url, requestOptions).then(response => {
+ return {
+ statusCode: response.status,
+ headers: {
+ 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
+ 'retry-after': response.headers.get('Retry-After'),
+ },
+ };
+ });
+ } catch (e) {
+ return rejectedSyncPromise(e);
+ }
+ }
+
+ return createTransport(options, makeRequest);
+}
diff --git a/packages/deno/src/types.ts b/packages/deno/src/types.ts
new file mode 100644
index 000000000000..50310589666a
--- /dev/null
+++ b/packages/deno/src/types.ts
@@ -0,0 +1,59 @@
+import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/types';
+
+import type { DenoTransportOptions } from './transports';
+
+export interface BaseDenoOptions {
+ /**
+ * List of strings/regex controlling to which outgoing requests
+ * the SDK will attach tracing headers.
+ *
+ * By default the SDK will attach those headers to all outgoing
+ * requests. If this option is provided, the SDK will match the
+ * request URL of outgoing requests against the items in this
+ * array, and only attach tracing headers if a match was found.
+ *
+ * @example
+ * ```js
+ * Sentry.init({
+ * tracePropagationTargets: ['api.site.com'],
+ * });
+ * ```
+ */
+ tracePropagationTargets?: TracePropagationTargets;
+
+ /** Sets an optional server name (device name) */
+ serverName?: string;
+
+ // TODO (v8): Remove this in v8
+ /**
+ * @deprecated Moved to constructor options of the `Http` and `Undici` integration.
+ * @example
+ * ```js
+ * Sentry.init({
+ * integrations: [
+ * new Sentry.Integrations.Http({
+ * tracing: {
+ * shouldCreateSpanForRequest: (url: string) => false,
+ * }
+ * });
+ * ],
+ * });
+ * ```
+ */
+ shouldCreateSpanForRequest?(this: void, url: string): boolean;
+
+ /** Callback that is executed when a fatal global error occurs. */
+ onFatalError?(this: void, error: Error): void;
+}
+
+/**
+ * Configuration options for the Sentry Deno SDK
+ * @see @sentry/types Options for more information.
+ */
+export interface DenoOptions extends Options, BaseDenoOptions {}
+
+/**
+ * Configuration options for the Sentry Deno SDK Client class
+ * @see DenoClient for more information.
+ */
+export interface DenoClientOptions extends ClientOptions, BaseDenoOptions {}
diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap
new file mode 100644
index 000000000000..749d3ce9d238
--- /dev/null
+++ b/packages/deno/test/__snapshots__/mod.test.ts.snap
@@ -0,0 +1,222 @@
+export const snapshot = {};
+
+snapshot[`captureException 1`] = `
+{
+ contexts: {
+ app: {
+ app_start_time: "{{time}}",
+ },
+ device: {
+ arch: "{{arch}}",
+ processor_count: 0,
+ },
+ os: {
+ name: "{{platform}}",
+ },
+ runtime: {
+ name: "deno",
+ version: "1.37.1",
+ },
+ trace: {
+ span_id: "{{id}}",
+ trace_id: "{{id}}",
+ },
+ typescript: {
+ name: "TypeScript",
+ version: "{{version}}",
+ },
+ v8: {
+ name: "v8",
+ version: "{{version}}",
+ },
+ },
+ environment: "production",
+ event_id: "{{id}}",
+ exception: {
+ values: [
+ {
+ mechanism: {
+ handled: true,
+ type: "generic",
+ },
+ stacktrace: {
+ frames: [
+ {
+ colno: 20,
+ filename: "ext:cli/40_testing.js",
+ function: "outerWrapped",
+ in_app: false,
+ lineno: 488,
+ },
+ {
+ colno: 33,
+ filename: "ext:cli/40_testing.js",
+ function: "exitSanitizer",
+ in_app: false,
+ lineno: 474,
+ },
+ {
+ colno: 31,
+ filename: "ext:cli/40_testing.js",
+ function: "resourceSanitizer",
+ in_app: false,
+ lineno: 425,
+ },
+ {
+ colno: 33,
+ filename: "ext:cli/40_testing.js",
+ function: "asyncOpSanitizer",
+ in_app: false,
+ lineno: 192,
+ },
+ {
+ colno: 11,
+ filename: "ext:cli/40_testing.js",
+ function: "innerWrapped",
+ in_app: false,
+ lineno: 543,
+ },
+ {
+ colno: 24,
+ context_line: " hub.captureException(something());",
+ filename: "app:///test/mod.test.ts",
+ function: "",
+ in_app: true,
+ lineno: 43,
+ post_context: [
+ "",
+ " await delay(200);",
+ " await assertSnapshot(t, ev);",
+ "});",
+ "",
+ "Deno.test('captureMessage', async t => {",
+ " let ev: Event | undefined;",
+ ],
+ pre_context: [
+ " ev = event;",
+ " });",
+ "",
+ " function something() {",
+ " return new Error('Some unhandled error');",
+ " }",
+ "",
+ ],
+ },
+ {
+ colno: 12,
+ context_line: " return new Error('Some unhandled error');",
+ filename: "app:///test/mod.test.ts",
+ function: "something",
+ in_app: true,
+ lineno: 40,
+ post_context: [
+ " }",
+ "",
+ " hub.captureException(something());",
+ "",
+ " await delay(200);",
+ " await assertSnapshot(t, ev);",
+ "});",
+ ],
+ pre_context: [
+ "Deno.test('captureException', async t => {",
+ " let ev: Event | undefined;",
+ " const [hub] = getTestClient(event => {",
+ " ev = event;",
+ " });",
+ "",
+ " function something() {",
+ ],
+ },
+ ],
+ },
+ type: "Error",
+ value: "Some unhandled error",
+ },
+ ],
+ },
+ platform: "deno",
+ sdk: {
+ integrations: [
+ "InboundFilters",
+ "FunctionToString",
+ "Dedupe",
+ "LinkedErrors",
+ "Breadcrumbs",
+ "DenoContext",
+ "ContextLines",
+ "NormalizePaths",
+ "GlobalHandlers",
+ ],
+ name: "sentry.javascript.deno",
+ packages: [
+ {
+ name: "denoland:sentry",
+ version: "{{version}}",
+ },
+ ],
+ version: "{{version}}",
+ },
+ timestamp: 0,
+}
+`;
+
+snapshot[`captureMessage 1`] = `
+{
+ contexts: {
+ app: {
+ app_start_time: "{{time}}",
+ },
+ device: {
+ arch: "{{arch}}",
+ processor_count: 0,
+ },
+ os: {
+ name: "{{platform}}",
+ },
+ runtime: {
+ name: "deno",
+ version: "1.37.1",
+ },
+ trace: {
+ span_id: "{{id}}",
+ trace_id: "{{id}}",
+ },
+ typescript: {
+ name: "TypeScript",
+ version: "{{version}}",
+ },
+ v8: {
+ name: "v8",
+ version: "{{version}}",
+ },
+ },
+ environment: "production",
+ event_id: "{{id}}",
+ level: "info",
+ message: "Some error message",
+ platform: "deno",
+ sdk: {
+ integrations: [
+ "InboundFilters",
+ "FunctionToString",
+ "Dedupe",
+ "LinkedErrors",
+ "Breadcrumbs",
+ "DenoContext",
+ "ContextLines",
+ "NormalizePaths",
+ "GlobalHandlers",
+ ],
+ name: "sentry.javascript.deno",
+ packages: [
+ {
+ name: "denoland:sentry",
+ version: "{{version}}",
+ },
+ ],
+ version: "{{version}}",
+ },
+ timestamp: 0,
+}
+`;
diff --git a/packages/deno/test/example.ts b/packages/deno/test/example.ts
new file mode 100644
index 000000000000..6f93bd288afd
--- /dev/null
+++ b/packages/deno/test/example.ts
@@ -0,0 +1,14 @@
+import * as Sentry from '../build/index.js';
+
+Sentry.init({
+ dsn: 'https://1234@some-domain.com/4505526893805568',
+});
+
+Sentry.addBreadcrumb({ message: 'My Breadcrumb' });
+
+// eslint-disable-next-line no-console
+console.log('App has started');
+
+setTimeout(() => {
+ Deno.exit();
+}, 1_000);
diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts
new file mode 100644
index 000000000000..1724125415cd
--- /dev/null
+++ b/packages/deno/test/mod.test.ts
@@ -0,0 +1,76 @@
+import { assertEquals } from 'https://deno.land/std@0.202.0/assert/assert_equals.ts';
+import { assertSnapshot } from 'https://deno.land/std@0.202.0/testing/snapshot.ts';
+import type { Event, Integration } from 'npm:@sentry/types';
+import { createStackParser, nodeStackLineParser } from 'npm:@sentry/utils';
+
+import { defaultIntegrations, DenoClient, Hub, Scope } from '../build/index.js';
+import { getNormalizedEvent } from './normalize.ts';
+import { makeTestTransport } from './transport.ts';
+
+function getTestClient(callback: (event?: Event) => void, integrations: Integration[] = []): [Hub, DenoClient] {
+ const client = new DenoClient({
+ dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507',
+ debug: true,
+ integrations: [...defaultIntegrations, ...integrations],
+ stackParser: createStackParser(nodeStackLineParser()),
+ transport: makeTestTransport(envelope => {
+ callback(getNormalizedEvent(envelope));
+ }),
+ });
+
+ const scope = new Scope();
+ const hub = new Hub(client, scope);
+
+ return [hub, client];
+}
+
+function delay(time: number): Promise {
+ return new Promise(resolve => {
+ setTimeout(resolve, time);
+ });
+}
+
+Deno.test('captureException', async t => {
+ let ev: Event | undefined;
+ const [hub] = getTestClient(event => {
+ ev = event;
+ });
+
+ function something() {
+ return new Error('Some unhandled error');
+ }
+
+ hub.captureException(something());
+
+ await delay(200);
+ await assertSnapshot(t, ev);
+});
+
+Deno.test('captureMessage', async t => {
+ let ev: Event | undefined;
+ const [hub] = getTestClient(event => {
+ ev = event;
+ });
+
+ hub.captureMessage('Some error message');
+
+ await delay(200);
+ await assertSnapshot(t, ev);
+});
+
+Deno.test('App runs without errors', async _ => {
+ const cmd = new Deno.Command('deno', {
+ args: ['run', '--allow-net=some-domain.com', './test/example.ts'],
+ stdout: 'piped',
+ stderr: 'piped',
+ });
+
+ const output = await cmd.output();
+ assertEquals(output.success, true);
+
+ const td = new TextDecoder();
+ const outString = td.decode(output.stdout);
+ const errString = td.decode(output.stderr);
+ assertEquals(outString, 'App has started\n');
+ assertEquals(errString, '');
+});
diff --git a/packages/deno/test/normalize.ts b/packages/deno/test/normalize.ts
new file mode 100644
index 000000000000..b36cf4ac52a9
--- /dev/null
+++ b/packages/deno/test/normalize.ts
@@ -0,0 +1,207 @@
+/* eslint-disable complexity */
+import type { Envelope, Event, Session, Transaction } from 'npm:@sentry/types';
+import { forEachEnvelopeItem } from 'npm:@sentry/utils';
+
+type EventOrSession = Event | Transaction | Session;
+
+export function getNormalizedEvent(envelope: Envelope): Event | undefined {
+ let event: Event | undefined;
+
+ forEachEnvelopeItem(envelope, item => {
+ const [headers, body] = item;
+
+ if (headers.type === 'event') {
+ event = body as Event;
+ }
+ });
+
+ return normalize(event) as Event | undefined;
+}
+
+export function normalize(event: EventOrSession | undefined): EventOrSession | undefined {
+ if (event === undefined) {
+ return undefined;
+ }
+
+ if (eventIsSession(event)) {
+ return normalizeSession(event as Session);
+ } else {
+ return normalizeEvent(event as Event);
+ }
+}
+
+export function eventIsSession(data: EventOrSession): boolean {
+ return !!(data as Session)?.sid;
+}
+
+/**
+ * Normalizes a session so that in can be compared to an expected event
+ *
+ * All properties that are timestamps, versions, ids or variables that may vary
+ * by platform are replaced with placeholder strings
+ */
+function normalizeSession(session: Session): Session {
+ if (session.sid) {
+ session.sid = '{{id}}';
+ }
+
+ if (session.started) {
+ session.started = 0;
+ }
+
+ if (session.timestamp) {
+ session.timestamp = 0;
+ }
+
+ if (session.duration) {
+ session.duration = 0;
+ }
+
+ return session;
+}
+
+/**
+ * Normalizes an event so that in can be compared to an expected event
+ *
+ * All properties that are timestamps, versions, ids or variables that may vary
+ * by platform are replaced with placeholder strings
+ */
+function normalizeEvent(event: Event): Event {
+ if (event.sdk?.version) {
+ event.sdk.version = '{{version}}';
+ }
+
+ if (event?.sdk?.packages) {
+ for (const pkg of event?.sdk?.packages) {
+ if (pkg.version) {
+ pkg.version = '{{version}}';
+ }
+ }
+ }
+
+ if (event.contexts?.app?.app_start_time) {
+ event.contexts.app.app_start_time = '{{time}}';
+ }
+
+ if (event.contexts?.typescript?.version) {
+ event.contexts.typescript.version = '{{version}}';
+ }
+
+ if (event.contexts?.v8?.version) {
+ event.contexts.v8.version = '{{version}}';
+ }
+
+ if (event.contexts?.deno) {
+ if (event.contexts.deno?.version) {
+ event.contexts.deno.version = '{{version}}';
+ }
+ if (event.contexts.deno?.target) {
+ event.contexts.deno.target = '{{target}}';
+ }
+ }
+
+ if (event.contexts?.device?.arch) {
+ event.contexts.device.arch = '{{arch}}';
+ }
+
+ if (event.contexts?.device?.memory_size) {
+ event.contexts.device.memory_size = 0;
+ }
+
+ if (event.contexts?.device?.free_memory) {
+ event.contexts.device.free_memory = 0;
+ }
+
+ if (event.contexts?.device?.processor_count) {
+ event.contexts.device.processor_count = 0;
+ }
+
+ if (event.contexts?.device?.processor_frequency) {
+ event.contexts.device.processor_frequency = 0;
+ }
+
+ if (event.contexts?.device?.cpu_description) {
+ event.contexts.device.cpu_description = '{{cpu}}';
+ }
+
+ if (event.contexts?.device?.screen_resolution) {
+ event.contexts.device.screen_resolution = '{{screen}}';
+ }
+
+ if (event.contexts?.device?.screen_density) {
+ event.contexts.device.screen_density = 1;
+ }
+
+ if (event.contexts?.device?.language) {
+ event.contexts.device.language = '{{language}}';
+ }
+
+ if (event.contexts?.os?.name) {
+ event.contexts.os.name = '{{platform}}';
+ }
+
+ if (event.contexts?.os?.version) {
+ event.contexts.os.version = '{{version}}';
+ }
+
+ if (event.contexts?.trace) {
+ event.contexts.trace.span_id = '{{id}}';
+ event.contexts.trace.trace_id = '{{id}}';
+ delete event.contexts.trace.tags;
+ }
+
+ if (event.start_timestamp) {
+ event.start_timestamp = 0;
+ }
+
+ if (event.exception?.values?.[0].stacktrace?.frames) {
+ // Exlcude Deno frames since these may change between versions
+ event.exception.values[0].stacktrace.frames = event.exception.values[0].stacktrace.frames.filter(
+ frame => !frame.filename?.includes('deno:'),
+ );
+ }
+
+ event.timestamp = 0;
+ // deno-lint-ignore no-explicit-any
+ if ((event as any).start_timestamp) {
+ // deno-lint-ignore no-explicit-any
+ (event as any).start_timestamp = 0;
+ }
+
+ event.event_id = '{{id}}';
+
+ if (event.spans) {
+ for (const span of event.spans) {
+ // deno-lint-ignore no-explicit-any
+ const spanAny = span as any;
+
+ if (spanAny.span_id) {
+ spanAny.span_id = '{{id}}';
+ }
+
+ if (spanAny.parent_span_id) {
+ spanAny.parent_span_id = '{{id}}';
+ }
+
+ if (spanAny.start_timestamp) {
+ spanAny.start_timestamp = 0;
+ }
+
+ if (spanAny.timestamp) {
+ spanAny.timestamp = 0;
+ }
+
+ if (spanAny.trace_id) {
+ spanAny.trace_id = '{{id}}';
+ }
+ }
+ }
+
+ if (event.breadcrumbs) {
+ for (const breadcrumb of event.breadcrumbs) {
+ breadcrumb.timestamp = 0;
+ }
+ }
+
+ return event;
+}
diff --git a/packages/deno/test/transport.ts b/packages/deno/test/transport.ts
new file mode 100644
index 000000000000..2eaeed6eeef6
--- /dev/null
+++ b/packages/deno/test/transport.ts
@@ -0,0 +1,30 @@
+import { createTransport } from 'npm:@sentry/core';
+import type {
+ BaseTransportOptions,
+ Envelope,
+ Transport,
+ TransportMakeRequestResponse,
+ TransportRequest,
+} from 'npm:@sentry/types';
+import { parseEnvelope } from 'npm:@sentry/utils';
+
+export interface TestTransportOptions extends BaseTransportOptions {
+ callback: (envelope: Envelope) => void;
+}
+
+/**
+ * Creates a Transport that uses the Fetch API to send events to Sentry.
+ */
+export function makeTestTransport(callback: (envelope: Envelope) => void) {
+ return (options: BaseTransportOptions): Transport => {
+ async function doCallback(request: TransportRequest): Promise {
+ await callback(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()));
+
+ return Promise.resolve({
+ statusCode: 200,
+ });
+ }
+
+ return createTransport(options, doCallback);
+ };
+}
diff --git a/packages/deno/tsconfig.build.json b/packages/deno/tsconfig.build.json
new file mode 100644
index 000000000000..87025d5676c5
--- /dev/null
+++ b/packages/deno/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["./lib.deno.d.ts", "src/**/*"],
+ "compilerOptions": {
+ "outDir": "build",
+ "lib": ["esnext"],
+ "module": "esnext",
+ "target": "esnext",
+ "declaration": true,
+ "declarationMap": false
+ }
+}
diff --git a/packages/deno/tsconfig.json b/packages/deno/tsconfig.json
new file mode 100644
index 000000000000..fdd107c1ed78
--- /dev/null
+++ b/packages/deno/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+ "include": ["./lib.deno.d.ts", "src/**/*", "example.ts"],
+ "compilerOptions": {
+ "lib": ["esnext"],
+ "module": "esnext",
+ "target": "esnext"
+ }
+}
diff --git a/packages/deno/tsconfig.test.json b/packages/deno/tsconfig.test.json
new file mode 100644
index 000000000000..548e94149758
--- /dev/null
+++ b/packages/deno/tsconfig.test.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["./lib.deno.d.ts", "test/**/*"],
+ "compilerOptions": {
+ "lib": ["esnext"],
+ "module": "esnext",
+ "target": "esnext"
+ }
+}
diff --git a/packages/deno/tsconfig.types.json b/packages/deno/tsconfig.types.json
new file mode 100644
index 000000000000..d6d1e9a548c9
--- /dev/null
+++ b/packages/deno/tsconfig.types.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build"
+ }
+}
diff --git a/scripts/node-unit-tests.ts b/scripts/node-unit-tests.ts
index dc923c8bf9a5..8824cee77d66 100644
--- a/scripts/node-unit-tests.ts
+++ b/scripts/node-unit-tests.ts
@@ -22,6 +22,7 @@ const DEFAULT_SKIP_TESTS_PACKAGES = [
'@sentry/replay',
'@sentry/wasm',
'@sentry/bun',
+ '@sentry/deno',
];
const SKIP_TEST_PACKAGES: Record = {
diff --git a/yarn.lock b/yarn.lock
index d52033c73e40..b8f2e0477bbd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4832,6 +4832,18 @@
magic-string "^0.25.7"
resolve "^1.17.0"
+"@rollup/plugin-commonjs@^25.0.5":
+ version "25.0.5"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.5.tgz#0bac8f985a5de151b4b09338847f8c7f20a28a29"
+ integrity sha512-xY8r/A9oisSeSuLCTfhssyDjo9Vp/eDiRLXkg1MXCcEEgEjPmLU+ZyDB20OOD0NlyDa/8SGbK5uIggF5XTx77w==
+ dependencies:
+ "@rollup/pluginutils" "^5.0.1"
+ commondir "^1.0.1"
+ estree-walker "^2.0.2"
+ glob "^8.0.3"
+ is-reference "1.2.1"
+ magic-string "^0.27.0"
+
"@rollup/plugin-json@^4.0.0", "@rollup/plugin-json@^4.1.0":
version "4.1.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-4.1.0.tgz#54e09867ae6963c593844d8bd7a9c718294496f3"
@@ -4891,6 +4903,14 @@
"@rollup/pluginutils" "^4.1.1"
sucrase "^3.20.0"
+"@rollup/plugin-typescript@^11.1.5":
+ version "11.1.5"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.5.tgz#039c763bf943a5921f3f42be255895e75764cb91"
+ integrity sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==
+ dependencies:
+ "@rollup/pluginutils" "^5.0.1"
+ resolve "^1.22.1"
+
"@rollup/plugin-typescript@^8.3.1":
version "8.5.0"
resolved "https://registry.yarnpkg.com/@rollup/plugin-typescript/-/plugin-typescript-8.5.0.tgz#7ea11599a15b0a30fa7ea69ce3b791d41b862515"
@@ -5993,6 +6013,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.38.tgz#f8bb07c371ccb1903f3752872c89f44006132947"
integrity sha512-5jY9RhV7c0Z4Jy09G+NIDTsCZ5G0L5n+Z+p+Y7t5VJHM30bgwzSjVtlcBxqAj+6L/swIlvtOSzr8rBk/aNyV2g==
+"@types/node@20.8.2":
+ version "20.8.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.2.tgz#d76fb80d87d0d8abfe334fc6d292e83e5524efc4"
+ integrity sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==
+
"@types/node@^10.1.0", "@types/node@~10.17.0":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
@@ -20053,7 +20078,7 @@ magic-string@^0.30.0:
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.13"
-magic-string@^0.30.3:
+magic-string@^0.30.3, magic-string@^0.30.4:
version "0.30.4"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.4.tgz#c2c683265fc18dda49b56fc7318d33ca0332c98c"
integrity sha512-Q/TKtsC5BPm0kGqgBIF9oXAs/xEf2vRKiIB4wCRQTJOQIByZ1d+NnUOotvJOvNpi5RNIgVOMC3pOuaP1ZTDlVg==
@@ -26520,6 +26545,15 @@ rollup-plugin-cleanup@3.2.1:
js-cleanup "^1.2.0"
rollup-pluginutils "^2.8.2"
+rollup-plugin-dts@^6.1.0:
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-6.1.0.tgz#56e9c5548dac717213c6a4aa9df523faf04f75ae"
+ integrity sha512-ijSCPICkRMDKDLBK9torss07+8dl9UpY9z1N/zTeA1cIqdzMlpkV3MOOC7zukyvQfDyxa1s3Dl2+DeiP/G6DOw==
+ dependencies:
+ magic-string "^0.30.4"
+ optionalDependencies:
+ "@babel/code-frame" "^7.22.13"
+
rollup-plugin-license@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/rollup-plugin-license/-/rollup-plugin-license-2.6.1.tgz#20f15cc37950f362f8eefdc6e3a2e659d0cad9eb"