|
1 |
| -import { Session as SessionInterface, SessionContext, SessionStatus } from '@sentry/types'; |
2 |
| -import { dropUndefinedKeys, uuid4 } from '@sentry/utils'; |
| 1 | +import { |
| 2 | + AggregationCounts, |
| 3 | + RequestSessionStatus, |
| 4 | + Session as SessionInterface, |
| 5 | + SessionAggregates, |
| 6 | + SessionContext, |
| 7 | + SessionFlusherLike, |
| 8 | + SessionStatus, |
| 9 | + Transport, |
| 10 | +} from '@sentry/types'; |
| 11 | +import { dropUndefinedKeys, logger, uuid4 } from '@sentry/utils'; |
| 12 | + |
| 13 | +import { getCurrentHub } from './hub'; |
3 | 14 |
|
4 | 15 | /**
|
5 | 16 | * @inheritdoc
|
@@ -123,3 +134,119 @@ export class Session implements SessionInterface {
|
123 | 134 | });
|
124 | 135 | }
|
125 | 136 | }
|
| 137 | + |
| 138 | +type ReleaseHealthAttributes = { |
| 139 | + environment?: string; |
| 140 | + release: string; |
| 141 | +}; |
| 142 | + |
| 143 | +/** |
| 144 | + * @inheritdoc |
| 145 | + */ |
| 146 | +export class SessionFlusher implements SessionFlusherLike { |
| 147 | + public readonly flushTimeout: number = 60; |
| 148 | + private _pendingAggregates: Record<number, AggregationCounts> = {}; |
| 149 | + private _sessionAttrs: ReleaseHealthAttributes; |
| 150 | + private _intervalId: ReturnType<typeof setInterval>; |
| 151 | + private _isEnabled: boolean = true; |
| 152 | + private _transport: Transport; |
| 153 | + |
| 154 | + constructor(transport: Transport, attrs: ReleaseHealthAttributes) { |
| 155 | + this._transport = transport; |
| 156 | + // Call to setInterval, so that flush is called every 60 seconds |
| 157 | + this._intervalId = setInterval(() => this.flush(), this.flushTimeout * 1000); |
| 158 | + this._sessionAttrs = attrs; |
| 159 | + } |
| 160 | + |
| 161 | + /** Sends session aggregates to Transport */ |
| 162 | + public sendSessionAggregates(sessionAggregates: SessionAggregates): void { |
| 163 | + if (!this._transport.sendSession) { |
| 164 | + logger.warn("Dropping session because custom transport doesn't implement sendSession"); |
| 165 | + return; |
| 166 | + } |
| 167 | + this._transport.sendSession(sessionAggregates).then(null, reason => { |
| 168 | + logger.error(`Error while sending session: ${reason}`); |
| 169 | + }); |
| 170 | + } |
| 171 | + |
| 172 | + /** Checks if `pendingAggregates` has entries, and if it does flushes them by calling `sendSessions` */ |
| 173 | + public flush(): void { |
| 174 | + const sessionAggregates = this.getSessionAggregates(); |
| 175 | + if (sessionAggregates.aggregates.length === 0) { |
| 176 | + return; |
| 177 | + } |
| 178 | + this._pendingAggregates = {}; |
| 179 | + this.sendSessionAggregates(sessionAggregates); |
| 180 | + } |
| 181 | + |
| 182 | + /** Massages the entries in `pendingAggregates` and returns aggregated sessions */ |
| 183 | + public getSessionAggregates(): SessionAggregates { |
| 184 | + const aggregates: AggregationCounts[] = Object.keys(this._pendingAggregates).map((key: string) => { |
| 185 | + return this._pendingAggregates[parseInt(key)]; |
| 186 | + }); |
| 187 | + |
| 188 | + const sessionAggregates: SessionAggregates = { |
| 189 | + attrs: this._sessionAttrs, |
| 190 | + aggregates, |
| 191 | + }; |
| 192 | + return dropUndefinedKeys(sessionAggregates); |
| 193 | + } |
| 194 | + |
| 195 | + /** JSDoc */ |
| 196 | + public close(): void { |
| 197 | + clearInterval(this._intervalId); |
| 198 | + this._isEnabled = false; |
| 199 | + this.flush(); |
| 200 | + } |
| 201 | + |
| 202 | + /** |
| 203 | + * Wrapper function for _incrementSessionStatusCount that checks if the instance of SessionFlusher is enabled then |
| 204 | + * fetches the session status of the request from `Scope.getRequestSession().status` on the scope and passes them to |
| 205 | + * `_incrementSessionStatusCount` along with the start date |
| 206 | + */ |
| 207 | + public incrementSessionStatusCount(): void { |
| 208 | + if (!this._isEnabled) { |
| 209 | + return; |
| 210 | + } |
| 211 | + const scope = getCurrentHub().getScope(); |
| 212 | + const requestSession = scope?.getRequestSession(); |
| 213 | + |
| 214 | + if (requestSession && requestSession.status) { |
| 215 | + this._incrementSessionStatusCount(requestSession.status, new Date()); |
| 216 | + // This is not entirely necessarily but is added as a safe guard to indicate the bounds of a request and so in |
| 217 | + // case captureRequestSession is called more than once to prevent double count |
| 218 | + scope?.setRequestSession(undefined); |
| 219 | + |
| 220 | + /* eslint-enable @typescript-eslint/no-unsafe-member-access */ |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | + /** |
| 225 | + * Increments status bucket in pendingAggregates buffer (internal state) corresponding to status of |
| 226 | + * the session received |
| 227 | + */ |
| 228 | + private _incrementSessionStatusCount(status: RequestSessionStatus, date: Date): number { |
| 229 | + // Truncate minutes and seconds on Session Started attribute to have one minute bucket keys |
| 230 | + const sessionStartedTrunc = new Date(date).setSeconds(0, 0); |
| 231 | + this._pendingAggregates[sessionStartedTrunc] = this._pendingAggregates[sessionStartedTrunc] || {}; |
| 232 | + |
| 233 | + // corresponds to aggregated sessions in one specific minute bucket |
| 234 | + // for example, {"started":"2021-03-16T08:00:00.000Z","exited":4, "errored": 1} |
| 235 | + const aggregationCounts: AggregationCounts = this._pendingAggregates[sessionStartedTrunc]; |
| 236 | + if (!aggregationCounts.started) { |
| 237 | + aggregationCounts.started = new Date(sessionStartedTrunc).toISOString(); |
| 238 | + } |
| 239 | + |
| 240 | + switch (status) { |
| 241 | + case RequestSessionStatus.Errored: |
| 242 | + aggregationCounts.errored = (aggregationCounts.errored || 0) + 1; |
| 243 | + return aggregationCounts.errored; |
| 244 | + case RequestSessionStatus.Ok: |
| 245 | + aggregationCounts.exited = (aggregationCounts.exited || 0) + 1; |
| 246 | + return aggregationCounts.exited; |
| 247 | + case RequestSessionStatus.Crashed: |
| 248 | + aggregationCounts.crashed = (aggregationCounts.crashed || 0) + 1; |
| 249 | + return aggregationCounts.crashed; |
| 250 | + } |
| 251 | + } |
| 252 | +} |
0 commit comments