-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(node): Release Health Session Aggregates #3319
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
d952992
8558c12
158d93d
5a9058e
42c6f9f
f1f11de
0e19077
f230bd8
022c427
16fd132
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 |
|---|---|---|
| @@ -1,9 +1,19 @@ | ||
| import { DebugMeta, Event, SentryRequest, TransactionSamplingMethod } from '@sentry/types'; | ||
|
|
||
| import { API } from '../../src/api'; | ||
| import { eventToSentryRequest } from '../../src/request'; | ||
| import { eventToSentryRequest, sessionToSentryRequest } from '../../src/request'; | ||
|
|
||
| const api = new API('https://[email protected]/12312012', { | ||
| sdk: { | ||
| integrations: ['AWSLambda'], | ||
| name: 'sentry.javascript.browser', | ||
| version: `12.31.12`, | ||
| packages: [{ name: 'npm:@sentry/browser', version: `12.31.12` }], | ||
| }, | ||
| }); | ||
|
|
||
| describe('eventToSentryRequest', () => { | ||
| let event: Event; | ||
| function parseEnvelopeRequest(request: SentryRequest): any { | ||
| const [envelopeHeaderString, itemHeaderString, eventString] = request.body.split('\n'); | ||
|
|
||
|
|
@@ -14,16 +24,6 @@ describe('eventToSentryRequest', () => { | |
| }; | ||
| } | ||
|
|
||
| const api = new API('https://[email protected]/12312012', { | ||
| sdk: { | ||
| integrations: ['AWSLambda'], | ||
| name: 'sentry.javascript.browser', | ||
| version: `12.31.12`, | ||
| packages: [{ name: 'npm:@sentry/browser', version: `12.31.12` }], | ||
| }, | ||
| }); | ||
| let event: Event; | ||
|
|
||
| beforeEach(() => { | ||
| event = { | ||
| contexts: { trace: { trace_id: '1231201211212012', span_id: '12261980', op: 'pageload' } }, | ||
|
|
@@ -125,3 +125,32 @@ describe('eventToSentryRequest', () => { | |
| ); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sessionToSentryRequest', () => { | ||
| it('test envelope creation for aggregateSessions', () => { | ||
| const aggregatedSession = { | ||
| attrs: { release: '1.0.x', environment: 'prod' }, | ||
| aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], | ||
| }; | ||
| const result = sessionToSentryRequest(aggregatedSession, api); | ||
|
|
||
| const [envelopeHeaderString, itemHeaderString, sessionString] = result.body.split('\n'); | ||
|
|
||
| expect(JSON.parse(envelopeHeaderString)).toEqual( | ||
| expect.objectContaining({ | ||
| sdk: { name: 'sentry.javascript.browser', version: '12.31.12' }, | ||
| }), | ||
| ); | ||
| expect(JSON.parse(itemHeaderString)).toEqual( | ||
| expect.objectContaining({ | ||
| type: 'sessions', | ||
| }), | ||
| ); | ||
| expect(JSON.parse(sessionString)).toEqual( | ||
| expect.objectContaining({ | ||
| attrs: { release: '1.0.x', environment: 'prod' }, | ||
| aggregates: [{ started: '2021-04-08T12:18:00.000Z', exited: 2 }], | ||
| }), | ||
| ); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,16 @@ | ||
| import { Session as SessionInterface, SessionContext, SessionStatus } from '@sentry/types'; | ||
| import { dropUndefinedKeys, uuid4 } from '@sentry/utils'; | ||
| import { | ||
| AggregationCounts, | ||
| RequestSessionStatus, | ||
| Session as SessionInterface, | ||
| SessionAggregates, | ||
| SessionContext, | ||
| SessionFlusherLike, | ||
| SessionStatus, | ||
| Transport, | ||
| } from '@sentry/types'; | ||
| import { dropUndefinedKeys, logger, uuid4 } from '@sentry/utils'; | ||
|
|
||
| import { getCurrentHub } from './hub'; | ||
|
|
||
| /** | ||
| * @inheritdoc | ||
|
|
@@ -123,3 +134,119 @@ export class Session implements SessionInterface { | |
| }); | ||
| } | ||
| } | ||
|
|
||
| type ReleaseHealthAttributes = { | ||
| environment?: string; | ||
| release: string; | ||
| }; | ||
|
|
||
| /** | ||
| * @inheritdoc | ||
| */ | ||
| export class SessionFlusher implements SessionFlusherLike { | ||
| public readonly flushTimeout: number = 60; | ||
| private _pendingAggregates: Record<number, AggregationCounts> = {}; | ||
| private _sessionAttrs: ReleaseHealthAttributes; | ||
| private _intervalId: ReturnType<typeof setInterval>; | ||
| private _isEnabled: boolean = true; | ||
| private _transport: Transport; | ||
|
|
||
| constructor(transport: Transport, attrs: ReleaseHealthAttributes) { | ||
| this._transport = transport; | ||
| // Call to setInterval, so that flush is called every 60 seconds | ||
| this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); | ||
| this._sessionAttrs = attrs; | ||
| } | ||
|
|
||
| /** Sends session aggregates to Transport */ | ||
| public sendSessionAggregates(sessionAggregates: SessionAggregates): void { | ||
| if (!this._transport.sendSession) { | ||
| logger.warn("Dropping session because custom transport doesn't implement sendSession"); | ||
| return; | ||
| } | ||
| this._transport.sendSession(sessionAggregates).then(null, reason => { | ||
| logger.error(`Error while sending session: ${reason}`); | ||
HazAT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
| } | ||
|
|
||
| /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` */ | ||
| public flush(): void { | ||
| const sessionAggregates = this.getSessionAggregates(); | ||
| if (sessionAggregates.aggregates.length === 0) { | ||
| return; | ||
| } | ||
| this._pendingAggregates = {}; | ||
| this.sendSessionAggregates(sessionAggregates); | ||
| } | ||
|
|
||
| /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ | ||
| public getSessionAggregates(): SessionAggregates { | ||
| const aggregates: AggregationCounts[] = Object.keys(this._pendingAggregates).map((key: string) => { | ||
| return this._pendingAggregates[parseInt(key)]; | ||
|
Member
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. Are the keys not typed as numbers? Why is it a string here?
Contributor
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. Keys are always coerced to strings, despite being stored as numbers, so
Contributor
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. lol, typescript u dumb :3
HazAT marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| const sessionAggregates: SessionAggregates = { | ||
| attrs: this._sessionAttrs, | ||
| aggregates, | ||
| }; | ||
| return dropUndefinedKeys(sessionAggregates); | ||
| } | ||
|
|
||
| /** JSDoc */ | ||
| public close(): void { | ||
| clearInterval(this._intervalId); | ||
| this._isEnabled = false; | ||
| this.flush(); | ||
| } | ||
|
|
||
| /** | ||
| * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then | ||
| * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to | ||
| * `_incrementSessionStatusCount` along with the start date | ||
| */ | ||
| public incrementSessionStatusCount(): void { | ||
| if (!this._isEnabled) { | ||
| return; | ||
| } | ||
| const scope = getCurrentHub().getScope(); | ||
| const requestSession = scope?.getRequestSession(); | ||
|
|
||
| if (requestSession && requestSession.status) { | ||
| this._incrementSessionStatusCount(requestSession.status, new Date()); | ||
| // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in | ||
| // case captureRequestSession is called more than once to prevent double count | ||
| scope?.setRequestSession(undefined); | ||
|
|
||
| /* eslint-enable @typescript-eslint/no-unsafe-member-access */ | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of | ||
| * the session received | ||
| */ | ||
| private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { | ||
| // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys | ||
| const sessionStartedTrunc = new Date(date).setSeconds(0, 0); | ||
| this._pendingAggregates[sessionStartedTrunc] = this._pendingAggregates[sessionStartedTrunc] || {}; | ||
|
|
||
| // corresponds to aggregated sessions in one specific minute bucket | ||
| // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} | ||
| const aggregationCounts: AggregationCounts = this._pendingAggregates[sessionStartedTrunc]; | ||
| if (!aggregationCounts.started) { | ||
| aggregationCounts.started = new Date(sessionStartedTrunc).toISOString(); | ||
| } | ||
|
|
||
| switch (status) { | ||
| case RequestSessionStatus.Errored: | ||
| aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; | ||
| return aggregationCounts.errored; | ||
| case RequestSessionStatus.Ok: | ||
| aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; | ||
| return aggregationCounts.exited; | ||
| case RequestSessionStatus.Crashed: | ||
| aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; | ||
| return aggregationCounts.crashed; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.