Skip to content

Commit 3c1e208

Browse files
authored
Fixes #85 Improvements to session management (Breaking Change) (#124)
* Fixes #85 Improvements to session management (Breaking Change) Added useSessions() * Sync compiler options * Provide better hinting around event type
1 parent a889cd2 commit 3c1e208

22 files changed

+141
-59
lines changed

example/browser/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ await Exceptionless.startup((c) => {
1313
c.updateSettingsWhenIdleInterval = 15000;
1414
c.usePersistedQueueStorage = true;
1515
c.setUserIdentity("12345678", "Blake");
16+
c.useSessions();
1617

1718
// set some default data
1819
c.defaultData["SampleUser"] = {

packages/angularjs/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"./package.json": "./package.json"
3434
},
3535
"scripts": {
36-
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2015 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2015 --format=esm --outfile=dist/index.bundle.min.js",
37-
"watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2015 --format=esm --watch --outfile=dist/index.bundle.js"
36+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.min.js",
37+
"watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js"
3838
},
3939
"sideEffects": false,
4040
"publishConfig": {

packages/browser/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"testEnvironment": "jsdom"
4646
},
4747
"scripts": {
48-
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2019 --format=esm --outfile=dist/index.bundle.min.js",
48+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.min.js",
4949
"watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js",
5050
"test": "jest"
5151
},

packages/browser/src/plugins/BrowserLifeCyclePlugin.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,19 @@ export class BrowserLifeCyclePlugin implements IEventPlugin {
1717

1818
this._client = context.client;
1919

20-
globalThis.addEventListener("beforeunload", () => void this._client?.suspend());
20+
globalThis.addEventListener("beforeunload", () => {
21+
if (this._client?.config.sessionsEnabled) {
22+
void this._client?.submitSessionEnd();
23+
}
24+
25+
void this._client?.suspend();
26+
});
27+
2128
document.addEventListener("visibilitychange", () => {
2229
if (document.visibilityState === 'visible') {
23-
void this._client?.startup()
30+
void this._client?.startup();
2431
} else {
25-
void this._client?.suspend()
32+
void this._client?.suspend();
2633
}
2734
});
2835

packages/browser/tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"compilerOptions": {
44
"lib": [
55
"DOM",
6-
"ES2020"
6+
"ES2021"
77
],
88
"outDir": "dist",
99
"rootDir": "src",

packages/core/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"testEnvironment": "jsdom"
4646
},
4747
"scripts": {
48-
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2019 --format=esm --outfile=dist/index.bundle.min.js",
48+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.min.js",
4949
"watch": "tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js",
5050
"test": "jest"
5151
},

packages/core/src/EventBuilder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ExceptionlessClient } from "./ExceptionlessClient.js";
2-
import { Event, KnownEventDataKeys } from "./models/Event.js";
2+
import { Event, EventType, KnownEventDataKeys } from "./models/Event.js";
33
import { ManualStackingInfo } from "./models/data/ManualStackingInfo.js";
44
import { UserInfo } from "./models/data/UserInfo.js";
55
import { EventContext } from "./models/EventContext.js";
@@ -19,7 +19,7 @@ export class EventBuilder {
1919
this.context = context || new EventContext();
2020
}
2121

22-
public setType(type: string): EventBuilder {
22+
public setType(type: EventType): EventBuilder {
2323
if (type) {
2424
this.target.type = type;
2525
}

packages/core/src/ExceptionlessClient.ts

+16-18
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export class ExceptionlessClient {
4646
// TODO: Can we schedule this as part of startup?
4747
await queue.process();
4848
}
49+
50+
if (this.config.sessionsEnabled) {
51+
await this.submitSessionStart();
52+
}
4953
}
5054

5155
/** Submit events, pause any timers and go into low power mode. */
@@ -175,27 +179,21 @@ export class ExceptionlessClient {
175179
return this.createSessionStart().submit();
176180
}
177181

178-
public async submitSessionEnd(sessionIdOrUserId: string): Promise<void> {
179-
if (sessionIdOrUserId && this.config.enabled && this.config.isValid) {
180-
this.config.services.log.info(
181-
`Submitting session end: ${sessionIdOrUserId}`,
182-
);
183-
await this.config.services.submissionClient.submitHeartbeat(
184-
sessionIdOrUserId,
185-
true,
186-
);
182+
public async submitSessionEnd(sessionIdOrUserId?: string): Promise<void> {
183+
const { currentSessionIdentifier, enabled, isValid, services } = this.config;
184+
const sessionId = sessionIdOrUserId || currentSessionIdentifier;
185+
if (sessionId && enabled && isValid) {
186+
services.log.info(`Submitting session end: ${sessionId}`);
187+
await services.submissionClient.submitHeartbeat(sessionId, true);
187188
}
188189
}
189190

190-
public async submitSessionHeartbeat(sessionIdOrUserId: string): Promise<void> {
191-
if (sessionIdOrUserId && this.config.enabled && this.config.isValid) {
192-
this.config.services.log.info(
193-
`Submitting session heartbeat: ${sessionIdOrUserId}`,
194-
);
195-
await this.config.services.submissionClient.submitHeartbeat(
196-
sessionIdOrUserId,
197-
false,
198-
);
191+
public async submitSessionHeartbeat(sessionIdOrUserId?: string): Promise<void> {
192+
const { currentSessionIdentifier, enabled, isValid, services } = this.config;
193+
const sessionId = sessionIdOrUserId || currentSessionIdentifier;
194+
if (sessionId && enabled && isValid) {
195+
services.log.info(`Submitting session heartbeat: ${sessionId}`);
196+
await services.submissionClient.submitHeartbeat(sessionId, false);
199197
}
200198
}
201199

packages/core/src/configuration/Configuration.ts

+40-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ConsoleLog } from "../logging/ConsoleLog.js";
55
import { NullLog } from "../logging/NullLog.js";
66
import { UserInfo } from "../models/data/UserInfo.js";
77
import { HeartbeatPlugin } from "../plugins/default/HeartbeatPlugin.js";
8+
import { SessionIdManagementPlugin } from "../plugins/default/SessionIdManagementPlugin.js";
89
import { EventPluginContext } from "../plugins/EventPluginContext.js";
910
import { EventPluginManager } from "../plugins/EventPluginManager.js";
1011
import { IEventPlugin } from "../plugins/IEventPlugin.js";
@@ -428,32 +429,24 @@ export class Configuration {
428429
}
429430

430431
/**
431-
* Set the default user identity for all events. If the heartbeat interval is
432-
* greater than 0 (default: 30000ms), heartbeats will be sent after the first
433-
* event submission.
432+
* Set the default user identity for all events.
434433
*/
435-
public setUserIdentity(userInfo: UserInfo, heartbeatInterval?: number): void;
436-
public setUserIdentity(identity: string, heartbeatInterval?: number): void;
437-
public setUserIdentity(identity: string, name: string, heartbeatInterval?: number): void;
438-
public setUserIdentity(userInfoOrIdentity: UserInfo | string, nameOrHeartbeatInterval?: string | number, heartbeatInterval: number = 30000): void {
439-
const name: string | undefined = typeof nameOrHeartbeatInterval === "string" ? nameOrHeartbeatInterval : undefined;
434+
public setUserIdentity(userInfo: UserInfo): void;
435+
public setUserIdentity(identity: string): void;
436+
public setUserIdentity(identity: string, name: string): void;
437+
public setUserIdentity(userInfoOrIdentity: UserInfo | string, name?: string): void {
440438
const userInfo: UserInfo = typeof userInfoOrIdentity !== "string"
441439
? userInfoOrIdentity
442440
: <UserInfo>{ identity: userInfoOrIdentity, name };
443441

444-
const interval: number = typeof nameOrHeartbeatInterval === "number" ? nameOrHeartbeatInterval : heartbeatInterval;
445-
const plugin = new HeartbeatPlugin(interval);
446-
447442
const shouldRemove: boolean = !userInfo || (!userInfo.identity && !userInfo.name);
448443
if (shouldRemove) {
449-
this.removePlugin(plugin)
450444
delete this.defaultData[KnownEventDataKeys.UserInfo];
451445
} else {
452-
this.addPlugin(plugin)
453446
this.defaultData[KnownEventDataKeys.UserInfo] = userInfo;
454447
}
455448

456-
this.services.log.info(`user identity: ${shouldRemove ? "null" : <string>userInfo.identity} (heartbeat interval: ${interval}ms)`);
449+
this.services.log.info(`user identity: ${shouldRemove ? "null" : <string>userInfo.identity}`);
457450
}
458451

459452
/**
@@ -477,7 +470,39 @@ export class Configuration {
477470
* This setting only works in environments that supports persisted storage.
478471
* There is also a performance penalty of extra IO/serialization.
479472
*/
480-
public usePersistedQueueStorage = false;
473+
public usePersistedQueueStorage: boolean = false;
474+
475+
/**
476+
* Gets or sets a value indicating whether to automatically send session start,
477+
* session heartbeats and session end events.
478+
*/
479+
public sessionsEnabled = false;
480+
481+
/**
482+
* Internal property used to track the current session identifier.
483+
*/
484+
public currentSessionIdentifier: string | null = null;
485+
486+
/**
487+
*
488+
* @param sendHeartbeats Controls whether heartbeat events are sent on an interval.
489+
* @param heartbeatInterval The interval at which heartbeats are sent after the last sent event. The default is 1 minutes.
490+
* @param useSessionIdManagement Allows you to manually control the session id. This is only recommended for single user desktop environments.
491+
*/
492+
public useSessions(sendHeartbeats: boolean = true, heartbeatInterval: number = 60000, useSessionIdManagement: boolean = false) {
493+
this.sessionsEnabled = true;
494+
495+
if (useSessionIdManagement) {
496+
this.addPlugin(new SessionIdManagementPlugin());
497+
}
498+
499+
const plugin = new HeartbeatPlugin(heartbeatInterval);
500+
if (sendHeartbeats) {
501+
this.addPlugin(plugin);
502+
} else {
503+
this.removePlugin(plugin);
504+
}
505+
}
481506

482507
private originalSettings?: Record<string, string>;
483508

packages/core/src/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export type { ILog } from "./logging/ILog.js";
88
export { ConsoleLog } from "./logging/ConsoleLog.js";
99
export { NullLog } from "./logging/NullLog.js";
1010

11-
export type { Event, IEventData } from "./models/Event.js";
11+
export type { Event, EventType, IEventData } from "./models/Event.js";
1212
export { KnownEventDataKeys } from "./models/Event.js";
1313
export type { EnvironmentInfo } from "./models/data/EnvironmentInfo.js";
1414
export type { ManualStackingInfo } from "./models/data/ManualStackingInfo.js";
@@ -30,6 +30,7 @@ export { DuplicateCheckerPlugin } from "./plugins/default/DuplicateCheckerPlugin
3030
export { EventExclusionPlugin } from "./plugins/default/EventExclusionPlugin.js";
3131
export { HeartbeatPlugin } from "./plugins/default/HeartbeatPlugin.js";
3232
export { ReferenceIdPlugin } from "./plugins/default/ReferenceIdPlugin.js";
33+
export { SessionIdManagementPlugin } from "./plugins/default/SessionIdManagementPlugin.js";
3334
export { IgnoredErrorProperties, SimpleErrorPlugin } from "./plugins/default/SimpleErrorPlugin.js"
3435
export { SubmissionMethodPlugin } from "./plugins/default/SubmissionMethodPlugin.js";
3536
export { EventContext } from "./models/EventContext.js";

packages/core/src/models/Event.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { UserInfo } from "./data/UserInfo.js";
55
import { UserDescription } from "./data/UserDescription.js";
66
import { ManualStackingInfo } from "./data/ManualStackingInfo.js";
77

8+
export type EventType = "error" | "usage" | "log" | "404" | "session" | string;
9+
810
export interface Event {
911
/** The event type (ie. error, log message, feature usage). */
10-
type?: string;
12+
type?: EventType;
1113
/** The event source (ie. machine name, log name, feature name). */
1214
source?: string;
1315
/** The date that the event occurred on. */

packages/core/src/plugins/default/HeartbeatPlugin.ts

+14-5
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class HeartbeatPlugin implements IEventPlugin {
99
private _interval: number;
1010
private _intervalId: ReturnType<typeof setInterval> | undefined;
1111

12-
constructor(heartbeatInterval: number = 30000) {
12+
constructor(heartbeatInterval: number = 60000) {
1313
this._interval = heartbeatInterval >= 30000 ? heartbeatInterval : 60000;
1414
}
1515

@@ -34,11 +34,20 @@ export class HeartbeatPlugin implements IEventPlugin {
3434
clearInterval(this._intervalId);
3535
this._intervalId = undefined;
3636

37-
const user = context.event.data?.[KnownEventDataKeys.UserInfo];
38-
if (user?.identity) {
37+
const { config } = context.client;
38+
if (!config.currentSessionIdentifier) {
39+
const user = context.event.data?.[KnownEventDataKeys.UserInfo];
40+
if (!user?.identity) {
41+
return Promise.resolve();
42+
}
43+
44+
config.currentSessionIdentifier = user.identity;
45+
}
46+
47+
if (config.currentSessionIdentifier) {
3948
this._intervalId = setInterval(
40-
() => void context.client.submitSessionHeartbeat(<string>user.identity),
41-
this._interval,
49+
() => void context.client.submitSessionHeartbeat(<string>config.currentSessionIdentifier),
50+
this._interval
4251
);
4352
}
4453

packages/core/src/plugins/default/ReferenceIdPlugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export class ReferenceIdPlugin implements IEventPlugin {
99
public run(context: EventPluginContext): Promise<void> {
1010
if (!context.event.reference_id && context.event.type === "error") {
1111
// PERF: Optimize identifier creation.
12-
context.event.reference_id = guid().replace("-", "").substring(0, 10);
12+
context.event.reference_id = guid().replaceAll("-", "").substring(0, 10);
1313
}
1414

1515
return Promise.resolve();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { guid } from "../../Utils.js";
2+
import { EventPluginContext } from "../EventPluginContext.js";
3+
import { IEventPlugin } from "../IEventPlugin.js";
4+
5+
export class SessionIdManagementPlugin implements IEventPlugin {
6+
public priority = 25;
7+
public name = "SessionIdManagementPlugin";
8+
9+
public run(context: EventPluginContext): Promise<void> {
10+
const ev = context.event;
11+
const isSessionStart: boolean = ev.type === "session";
12+
const { config } = context.client;
13+
if (isSessionStart || !config.currentSessionIdentifier) {
14+
config.currentSessionIdentifier = guid().replaceAll("-", "");
15+
}
16+
17+
if (isSessionStart) {
18+
ev.reference_id = config.currentSessionIdentifier;
19+
} else {
20+
if (!ev.data) {
21+
ev.data = {};
22+
}
23+
24+
ev.data["@ref:session"] = config.currentSessionIdentifier;
25+
}
26+
27+
return Promise.resolve();
28+
}
29+
}

packages/core/test/plugins/default/EventExclusionPlugin.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, test } from "@jest/globals";
22
import { expect } from "expect";
33

44
import { ExceptionlessClient } from "../../../src/ExceptionlessClient.js";
5-
import { Event, KnownEventDataKeys } from "../../../src/models/Event.js";
5+
import { Event, EventType, KnownEventDataKeys } from "../../../src/models/Event.js";
66
import { InnerErrorInfo } from "../../../src/models/data/ErrorInfo.js";
77
import { EventExclusionPlugin } from "../../../src/plugins/default/EventExclusionPlugin.js";
88
import { EventPluginContext } from "../../../src/plugins/EventPluginContext.js";
@@ -142,7 +142,7 @@ describe("EventExclusionPlugin", () => {
142142
});
143143

144144
describe("should exclude source type", () => {
145-
const run = async (type: string | null | undefined, source: string | undefined, settingKey: string | null | undefined, settingValue: string | null | undefined): Promise<boolean> => {
145+
const run = async (type: EventType | null | undefined, source: string | undefined, settingKey: string | null | undefined, settingValue: string | null | undefined): Promise<boolean> => {
146146
const client = new ExceptionlessClient();
147147

148148
if (typeof settingKey === "string") {

packages/core/test/submission/TestSubmissionClient.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe("TestSubmissionClient", () => {
2020
const apiFetchMock = jest.fn<(url: string, options: FetchOptions) => Promise<Response<undefined>>>()
2121
.mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, NaN, undefined)));
2222

23-
const events = [{ type: "log", message: "From js client", reference_id: "123454321" }];
23+
const events: Event[] = [{ type: "log", message: "From js client", reference_id: "123454321" }];
2424
const client = new TestSubmissionClient(config, apiFetchMock);
2525
await client.submitEvents(events);
2626
expect(apiFetchMock).toHaveBeenCalledTimes(1);

packages/node/src/plugins/NodeLifeCyclePlugin.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class NodeLifeCyclePlugin implements IEventPlugin {
2323
void this._client?.submitLog("beforeExit", message, "Error");
2424
}
2525

26+
if (this._client?.config.sessionsEnabled) {
27+
void this._client?.submitSessionEnd();
28+
}
29+
2630
void this._client?.suspend();
2731
// Application will now exit.
2832
});

packages/react/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"./package.json": "./package.json"
3434
},
3535
"scripts": {
36-
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2019 --format=esm --outfile=dist/index.bundle.min.js",
36+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.min.js",
3737
"watch": "tsc -p ../core/tsconfig.json -w --preserveWatchOutput & tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js"
3838
},
3939
"sideEffects": false,

packages/react/tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
{
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
4-
"lib": ["DOM", "ES2020"],
4+
"lib": [
5+
"DOM",
6+
"ES2021"
7+
],
58
"outDir": "dist",
69
"rootDir": "src",
710
"jsx": "react",

packages/vue/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"./package.json": "./package.json"
3434
},
3535
"scripts": {
36-
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2019 --format=esm --outfile=dist/index.bundle.min.js",
36+
"build": "tsc -p tsconfig.json && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.js && esbuild src/index.ts --bundle --minify --sourcemap --target=es2017 --format=esm --outfile=dist/index.bundle.min.js",
3737
"watch": "tsc -p tsconfig.json -w --preserveWatchOutput & && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js &"
3838
},
3939
"sideEffects": false,

0 commit comments

Comments
 (0)