Skip to content

Commit

Permalink
feat(node)!: Collect request sessions via HTTP instrumentation (#14658)
Browse files Browse the repository at this point in the history
  • Loading branch information
lforst authored Dec 19, 2024
1 parent 6d006bf commit 117a3b4
Show file tree
Hide file tree
Showing 25 changed files with 366 additions and 1,060 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init'),
gzip: true,
limit: '24 KB',
limit: '24.1 KB',
modifyWebpackConfig: function (config) {
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
Expand Down
15 changes: 6 additions & 9 deletions dev-packages/node-integration-tests/suites/sessions/server.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import type { SessionFlusher } from '@sentry/core';
import * as Sentry from '@sentry/node';

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
transport: loggingTransport,
integrations: [
Sentry.httpIntegration({
// Flush after 2 seconds (to avoid waiting for the default 60s)
sessionFlushingDelayMS: 2_000,
}),
],
});

import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
import express from 'express';

const app = express();

// eslint-disable-next-line deprecation/deprecation
const flusher = (Sentry.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher;

// Flush after 2 seconds (to avoid waiting for the default 60s)
setTimeout(() => {
flusher?.flush();
}, 2000);

app.get('/test/success', (_req, res) => {
res.send('Success!');
});
Expand Down
1 change: 1 addition & 0 deletions docs/migration/v8-to-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ The Sentry metrics beta has ended and the metrics API has been removed from the
- Deprecated `RequestSessionStatus` type. No replacements.
- Deprecated `SessionFlusherLike` type. No replacements.
- Deprecated `SessionFlusher`. No replacements.
- Deprecated `initSessionFlusher` on `ServerRuntimeClient`. No replacements. The `httpIntegration` will flush sessions by itself.

## `@sentry/nestjs`

Expand Down
31 changes: 24 additions & 7 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import type {
} from './types-hoist';

import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
import { DEFAULT_ENVIRONMENT } from './constants';
import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes';
import { DEBUG_BUILD } from './debug-build';
import { createEventEnvelope, createSessionEnvelope } from './envelope';
Expand All @@ -55,6 +56,7 @@ import { prepareEvent } from './utils/prepareEvent';
import { showSpanDropWarning } from './utils/spanUtils';

const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
const MISSING_RELEASE_FOR_SESSION_ERROR = 'Discarded session because of missing or non-string release';

/**
* Base implementation for all JavaScript SDK clients.
Expand Down Expand Up @@ -236,13 +238,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
* @inheritDoc
*/
public captureSession(session: Session): void {
if (!(typeof session.release === 'string')) {
DEBUG_BUILD && logger.warn('Discarded session because of missing or non-string release');
} else {
this.sendSession(session);
// After sending, we set init false to indicate it's not the first occurrence
updateSession(session, { init: false });
}
this.sendSession(session);
// After sending, we set init false to indicate it's not the first occurrence
updateSession(session, { init: false });
}

/**
Expand Down Expand Up @@ -371,6 +369,25 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
* @inheritDoc
*/
public sendSession(session: Session | SessionAggregates): void {
// Backfill release and environment on session
const { release: clientReleaseOption, environment: clientEnvironmentOption = DEFAULT_ENVIRONMENT } = this._options;
if ('aggregates' in session) {
const sessionAttrs = session.attrs || {};
if (!sessionAttrs.release && !clientReleaseOption) {
DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR);
return;
}
sessionAttrs.release = sessionAttrs.release || clientReleaseOption;
sessionAttrs.environment = sessionAttrs.environment || clientEnvironmentOption;
} else {
if (!session.release && !clientReleaseOption) {
DEBUG_BUILD && logger.warn(MISSING_RELEASE_FOR_SESSION_ERROR);
return;
}
session.release = session.release || clientReleaseOption;
session.environment = session.environment || clientEnvironmentOption;
}

const env = createSessionEnvelope(session, this._dsn, this._options._metadata, this._options.tunnel);

// sendEnvelope should not throw
Expand Down
6 changes: 0 additions & 6 deletions packages/core/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import type {
User,
} from './types-hoist';

import { DEFAULT_ENVIRONMENT } from './constants';
import { getClient, getCurrentScope, getIsolationScope, withIsolationScope } from './currentScopes';
import { DEBUG_BUILD } from './debug-build';
import type { CaptureContext } from './scope';
Expand Down Expand Up @@ -265,18 +264,13 @@ export function addEventProcessor(callback: EventProcessor): void {
* @returns the new active session
*/
export function startSession(context?: SessionContext): Session {
const client = getClient();
const isolationScope = getIsolationScope();
const currentScope = getCurrentScope();

const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {};

// Will fetch userAgent if called from browser sdk
const { userAgent } = GLOBAL_OBJ.navigator || {};

const session = makeSession({
release,
environment,
user: currentScope.getUser() || isolationScope.getUser(),
...(userAgent && { userAgent }),
...context,
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ export {
export { setAsyncContextStrategy } from './asyncContext';
export { getGlobalSingleton, getMainCarrier } from './carrier';
export { makeSession, closeSession, updateSession } from './session';
// eslint-disable-next-line deprecation/deprecation
export { SessionFlusher } from './sessionflusher';
export { Scope } from './scope';
export type { CaptureContext, ScopeContext, ScopeData } from './scope';
export { notifyEventProcessors } from './eventProcessors';
Expand Down
55 changes: 14 additions & 41 deletions packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
Extras,
Primitive,
PropagationContext,
RequestSession,
Session,
SeverityLevel,
Span,
Expand Down Expand Up @@ -50,11 +49,17 @@ export interface ScopeContext {
contexts: Contexts;
tags: { [key: string]: Primitive };
fingerprint: string[];
// eslint-disable-next-line deprecation/deprecation
requestSession: RequestSession;
propagationContext: PropagationContext;
}

// TODO(v9): Add `normalizedRequest`
export interface SdkProcessingMetadata {
[key: string]: unknown;
requestSession?: {
status: 'ok' | 'errored' | 'crashed';
};
}

/**
* Normalized data of the Scope, ready to be used.
*/
Expand All @@ -67,7 +72,7 @@ export interface ScopeData {
contexts: Contexts;
attachments: Attachment[];
propagationContext: PropagationContext;
sdkProcessingMetadata: { [key: string]: unknown };
sdkProcessingMetadata: SdkProcessingMetadata;
fingerprint: string[];
level?: SeverityLevel;
transactionName?: string;
Expand Down Expand Up @@ -112,7 +117,7 @@ export class Scope {
* A place to stash data which is needed at some point in the SDK's event processing pipeline but which shouldn't get
* sent to Sentry
*/
protected _sdkProcessingMetadata: { [key: string]: unknown };
protected _sdkProcessingMetadata: SdkProcessingMetadata;

/** Fingerprint */
protected _fingerprint?: string[];
Expand All @@ -131,10 +136,6 @@ export class Scope {
/** Session */
protected _session?: Session;

/** Request Mode Session Status */
// eslint-disable-next-line deprecation/deprecation
protected _requestSession?: RequestSession;

/** The client on this scope */
protected _client?: Client;

Expand Down Expand Up @@ -183,7 +184,6 @@ export class Scope {
newScope._transactionName = this._transactionName;
newScope._fingerprint = this._fingerprint;
newScope._eventProcessors = [...this._eventProcessors];
newScope._requestSession = this._requestSession;
newScope._attachments = [...this._attachments];
newScope._sdkProcessingMetadata = { ...this._sdkProcessingMetadata };
newScope._propagationContext = { ...this._propagationContext };
Expand Down Expand Up @@ -271,27 +271,6 @@ export class Scope {
return this._user;
}

/**
* Get the request session from this scope.
*
* @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`;
*/
// eslint-disable-next-line deprecation/deprecation
public getRequestSession(): RequestSession | undefined {
return this._requestSession;
}

/**
* Set the request session for this scope.
*
* @deprecated Use `getSession()` and `setSession()` instead of `getRequestSession()` and `setRequestSession()`;
*/
// eslint-disable-next-line deprecation/deprecation
public setRequestSession(requestSession?: RequestSession): this {
this._requestSession = requestSession;
return this;
}

/**
* Set an object that will be merged into existing tags on the scope,
* and will be sent as tags data with the event.
Expand Down Expand Up @@ -422,13 +401,12 @@ export class Scope {

const scopeToMerge = typeof captureContext === 'function' ? captureContext(this) : captureContext;

const [scopeInstance, requestSession] =
const scopeInstance =
scopeToMerge instanceof Scope
? // eslint-disable-next-line deprecation/deprecation
[scopeToMerge.getScopeData(), scopeToMerge.getRequestSession()]
? scopeToMerge.getScopeData()
: isPlainObject(scopeToMerge)
? [captureContext as ScopeContext, (captureContext as ScopeContext).requestSession]
: [];
? (captureContext as ScopeContext)
: undefined;

const { tags, extra, user, contexts, level, fingerprint = [], propagationContext } = scopeInstance || {};

Expand All @@ -452,10 +430,6 @@ export class Scope {
this._propagationContext = propagationContext;
}

if (requestSession) {
this._requestSession = requestSession;
}

return this;
}

Expand All @@ -473,7 +447,6 @@ export class Scope {
this._level = undefined;
this._transactionName = undefined;
this._fingerprint = undefined;
this._requestSession = undefined;
this._session = undefined;
_setSpanForScope(this, undefined);
this._attachments = [];
Expand Down
Loading

0 comments on commit 117a3b4

Please sign in to comment.