diff --git a/.gitignore b/.gitignore index ce8be2090e76..8e48f9bff776 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ tmp.js packages/deno/build-types packages/deno/build-test packages/deno/lib.deno.d.ts +deno.lock # gatsby packages/gatsby/gatsby-node.d.ts diff --git a/codecov.yml b/codecov.yml index fcc0885b060b..3a0292ae5d3c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -14,3 +14,6 @@ coverage: patch: default: enabled: no + +ignore: + - 'packages/deno/**' diff --git a/packages/deno/src/client.ts b/packages/deno/src/client.ts index b87cfcc6e163..36886c8d5a5e 100644 --- a/packages/deno/src/client.ts +++ b/packages/deno/src/client.ts @@ -1,5 +1,5 @@ import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; +import { _INTERNAL_flushLogsBuffer, SDK_VERSION, ServerRuntimeClient } from '@sentry/core'; import type { DenoClientOptions } from './types'; function getHostName(): string | undefined { @@ -19,6 +19,8 @@ function getHostName(): string | undefined { * @see SentryClient for usage documentation. */ export class DenoClient extends ServerRuntimeClient { + private _logOnExitFlushListener: (() => void) | undefined; + /** * Creates a new Deno SDK instance. * @param options Configuration options for this SDK. @@ -36,13 +38,42 @@ export class DenoClient extends ServerRuntimeClient { version: SDK_VERSION, }; + const serverName = options.serverName || getHostName(); + const clientOptions: ServerRuntimeClientOptions = { ...options, platform: 'javascript', runtime: { name: 'deno', version: Deno.version.deno }, - serverName: options.serverName || getHostName(), + serverName, }; super(clientOptions); + + if (this.getOptions().enableLogs) { + this._logOnExitFlushListener = () => { + _INTERNAL_flushLogsBuffer(this); + }; + + if (serverName) { + this.on('beforeCaptureLog', log => { + log.attributes = { + ...log.attributes, + 'server.address': serverName, + }; + }); + } + + globalThis.addEventListener('unload', this._logOnExitFlushListener); + } + } + + /** @inheritDoc */ + // @ts-expect-error - PromiseLike is a subset of Promise + public async close(timeout?: number | undefined): PromiseLike { + if (this._logOnExitFlushListener) { + globalThis.removeEventListener('unload', this._logOnExitFlushListener); + } + + return super.close(timeout); } } diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 5f987b4459aa..e698b461c4fc 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -1,6 +1,8 @@ export type { Breadcrumb, BreadcrumbHint, + Log, + LogSeverityLevel, PolymorphicRequest, RequestEventData, SdkInfo, @@ -89,6 +91,8 @@ export { updateSpanName, wrapMcpServerWithSentry, featureFlagsIntegration, + logger, + consoleLoggingIntegration, } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index 0d40945951c4..891cd107b7b7 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -1,8 +1,8 @@ -import type { Event } from '@sentry/core'; -import { createStackParser, nodeStackLineParser } from '@sentry/core'; +import type { Envelope, Event, Log } from '@sentry/core'; +import { createStackParser, forEachEnvelopeItem, nodeStackLineParser } from '@sentry/core'; 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 { DenoClient, getCurrentScope, getDefaultIntegrations } from '../build/esm/index.js'; +import { DenoClient, getCurrentScope, getDefaultIntegrations, logger, Scope } from '../build/esm/index.js'; import { getNormalizedEvent } from './normalize.ts'; import { makeTestTransport } from './transport.ts'; @@ -74,6 +74,102 @@ Deno.test('captureMessage twice', async t => { await assertSnapshot(t, ev); }); +Deno.test('logger.info captures a log envelope item', async () => { + const envelopes: Array = []; + const client = new DenoClient({ + dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', + enableLogs: true, + integrations: getDefaultIntegrations({}), + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(envelope => { + envelopes.push(envelope); + }), + }); + + client.init(); + const scope = new Scope(); + scope.setClient(client); + + logger.info('test log message', { key: 'value' }, { scope }); + + await client.flush(2000); + + // deno-lint-ignore no-explicit-any + let logItem: any = undefined; + for (const envelope of envelopes) { + forEachEnvelopeItem(envelope, item => { + const [headers, body] = item; + if (headers.type === 'log') { + logItem = body; + } + }); + } + + assertEquals(logItem !== undefined, true); + assertEquals(logItem.items.length, 1); + assertEquals(logItem.items[0].level, 'info'); + assertEquals(logItem.items[0].body, 'test log message'); +}); + +Deno.test('adds server.address to log attributes', () => { + const client = new DenoClient({ + dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', + enableLogs: true, + serverName: 'test-server', + integrations: getDefaultIntegrations({}), + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(() => {}), + }); + + const log: Log = { level: 'info', message: 'test message', attributes: {} }; + client.emit('beforeCaptureLog', log); + + assertEquals(log.attributes?.['server.address'], 'test-server'); +}); + +Deno.test('preserves existing log attributes when adding server.address', () => { + const client = new DenoClient({ + dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', + enableLogs: true, + serverName: 'test-server', + integrations: getDefaultIntegrations({}), + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(() => {}), + }); + + const log: Log = { level: 'info', message: 'test message', attributes: { 'existing.attr': 'value' } }; + client.emit('beforeCaptureLog', log); + + assertEquals(log.attributes?.['existing.attr'], 'value'); + assertEquals(log.attributes?.['server.address'], 'test-server'); +}); + +Deno.test('close() removes unload listener when enableLogs is true', async () => { + const removeEventListenerCalls: Array = []; + const originalRemoveEventListener = globalThis.removeEventListener; + globalThis.removeEventListener = ((event: string, ...args: unknown[]) => { + removeEventListenerCalls.push(event); + // deno-lint-ignore no-explicit-any + return originalRemoveEventListener.call(globalThis, event, ...(args as [any])); + }) as typeof globalThis.removeEventListener; + + try { + const client = new DenoClient({ + dsn: 'https://233a45e5efe34c47a3536797ce15dafa@nothing.here/5650507', + enableLogs: true, + integrations: getDefaultIntegrations({}), + stackParser: createStackParser(nodeStackLineParser()), + transport: makeTestTransport(() => {}), + }); + + await client.close(); + + assertEquals(removeEventListenerCalls.includes('unload'), true); + } finally { + globalThis.removeEventListener = originalRemoveEventListener; + } +}); + Deno.test('App runs without errors', async _ => { const cmd = new Deno.Command('deno', { args: ['run', '--allow-net=some-domain.com', './test/example.ts'],