diff --git a/example/browser/index.js b/example/browser/index.js index 73a66904..2426cc45 100644 --- a/example/browser/index.js +++ b/example/browser/index.js @@ -13,6 +13,7 @@ await Exceptionless.startup((c) => { c.updateSettingsWhenIdleInterval = 15000; c.usePersistedQueueStorage = true; c.setUserIdentity("12345678", "Blake"); + c.useSessions(); // set some default data c.defaultData["SampleUser"] = { diff --git a/packages/angularjs/package.json b/packages/angularjs/package.json index dddd7bc1..7b571db2 100644 --- a/packages/angularjs/package.json +++ b/packages/angularjs/package.json @@ -33,8 +33,8 @@ "./package.json": "./package.json" }, "scripts": { - "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", - "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" + "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", + "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" }, "sideEffects": false, "publishConfig": { diff --git a/packages/browser/package.json b/packages/browser/package.json index e1a1621a..0838eced 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -45,7 +45,7 @@ "testEnvironment": "jsdom" }, "scripts": { - "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", + "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", "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", "test": "jest" }, diff --git a/packages/browser/src/plugins/BrowserLifeCyclePlugin.ts b/packages/browser/src/plugins/BrowserLifeCyclePlugin.ts index 7c6188a5..c1f98065 100644 --- a/packages/browser/src/plugins/BrowserLifeCyclePlugin.ts +++ b/packages/browser/src/plugins/BrowserLifeCyclePlugin.ts @@ -17,12 +17,19 @@ export class BrowserLifeCyclePlugin implements IEventPlugin { this._client = context.client; - globalThis.addEventListener("beforeunload", () => void this._client?.suspend()); + globalThis.addEventListener("beforeunload", () => { + if (this._client?.config.sessionsEnabled) { + void this._client?.submitSessionEnd(); + } + + void this._client?.suspend(); + }); + document.addEventListener("visibilitychange", () => { if (document.visibilityState === 'visible') { - void this._client?.startup() + void this._client?.startup(); } else { - void this._client?.suspend() + void this._client?.suspend(); } }); diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index 90cfe5ba..8e36fb94 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "lib": [ "DOM", - "ES2020" + "ES2021" ], "outDir": "dist", "rootDir": "src", diff --git a/packages/core/package.json b/packages/core/package.json index ecdbfcf3..ed777153 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -45,7 +45,7 @@ "testEnvironment": "jsdom" }, "scripts": { - "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", + "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", "watch": "tsc -p tsconfig.json -w --preserveWatchOutput & esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js", "test": "jest" }, diff --git a/packages/core/src/EventBuilder.ts b/packages/core/src/EventBuilder.ts index 4e43a1a3..f81316ba 100644 --- a/packages/core/src/EventBuilder.ts +++ b/packages/core/src/EventBuilder.ts @@ -1,5 +1,5 @@ import { ExceptionlessClient } from "./ExceptionlessClient.js"; -import { Event, KnownEventDataKeys } from "./models/Event.js"; +import { Event, EventType, KnownEventDataKeys } from "./models/Event.js"; import { ManualStackingInfo } from "./models/data/ManualStackingInfo.js"; import { UserInfo } from "./models/data/UserInfo.js"; import { EventContext } from "./models/EventContext.js"; @@ -19,7 +19,7 @@ export class EventBuilder { this.context = context || new EventContext(); } - public setType(type: string): EventBuilder { + public setType(type: EventType): EventBuilder { if (type) { this.target.type = type; } diff --git a/packages/core/src/ExceptionlessClient.ts b/packages/core/src/ExceptionlessClient.ts index 350ce1bf..d652c635 100644 --- a/packages/core/src/ExceptionlessClient.ts +++ b/packages/core/src/ExceptionlessClient.ts @@ -46,6 +46,10 @@ export class ExceptionlessClient { // TODO: Can we schedule this as part of startup? await queue.process(); } + + if (this.config.sessionsEnabled) { + await this.submitSessionStart(); + } } /** Submit events, pause any timers and go into low power mode. */ @@ -175,27 +179,21 @@ export class ExceptionlessClient { return this.createSessionStart().submit(); } - public async submitSessionEnd(sessionIdOrUserId: string): Promise { - if (sessionIdOrUserId && this.config.enabled && this.config.isValid) { - this.config.services.log.info( - `Submitting session end: ${sessionIdOrUserId}`, - ); - await this.config.services.submissionClient.submitHeartbeat( - sessionIdOrUserId, - true, - ); + public async submitSessionEnd(sessionIdOrUserId?: string): Promise { + const { currentSessionIdentifier, enabled, isValid, services } = this.config; + const sessionId = sessionIdOrUserId || currentSessionIdentifier; + if (sessionId && enabled && isValid) { + services.log.info(`Submitting session end: ${sessionId}`); + await services.submissionClient.submitHeartbeat(sessionId, true); } } - public async submitSessionHeartbeat(sessionIdOrUserId: string): Promise { - if (sessionIdOrUserId && this.config.enabled && this.config.isValid) { - this.config.services.log.info( - `Submitting session heartbeat: ${sessionIdOrUserId}`, - ); - await this.config.services.submissionClient.submitHeartbeat( - sessionIdOrUserId, - false, - ); + public async submitSessionHeartbeat(sessionIdOrUserId?: string): Promise { + const { currentSessionIdentifier, enabled, isValid, services } = this.config; + const sessionId = sessionIdOrUserId || currentSessionIdentifier; + if (sessionId && enabled && isValid) { + services.log.info(`Submitting session heartbeat: ${sessionId}`); + await services.submissionClient.submitHeartbeat(sessionId, false); } } diff --git a/packages/core/src/configuration/Configuration.ts b/packages/core/src/configuration/Configuration.ts index 6a934865..dd02ce51 100644 --- a/packages/core/src/configuration/Configuration.ts +++ b/packages/core/src/configuration/Configuration.ts @@ -5,6 +5,7 @@ import { ConsoleLog } from "../logging/ConsoleLog.js"; import { NullLog } from "../logging/NullLog.js"; import { UserInfo } from "../models/data/UserInfo.js"; import { HeartbeatPlugin } from "../plugins/default/HeartbeatPlugin.js"; +import { SessionIdManagementPlugin } from "../plugins/default/SessionIdManagementPlugin.js"; import { EventPluginContext } from "../plugins/EventPluginContext.js"; import { EventPluginManager } from "../plugins/EventPluginManager.js"; import { IEventPlugin } from "../plugins/IEventPlugin.js"; @@ -428,32 +429,24 @@ export class Configuration { } /** - * Set the default user identity for all events. If the heartbeat interval is - * greater than 0 (default: 30000ms), heartbeats will be sent after the first - * event submission. + * Set the default user identity for all events. */ - public setUserIdentity(userInfo: UserInfo, heartbeatInterval?: number): void; - public setUserIdentity(identity: string, heartbeatInterval?: number): void; - public setUserIdentity(identity: string, name: string, heartbeatInterval?: number): void; - public setUserIdentity(userInfoOrIdentity: UserInfo | string, nameOrHeartbeatInterval?: string | number, heartbeatInterval: number = 30000): void { - const name: string | undefined = typeof nameOrHeartbeatInterval === "string" ? nameOrHeartbeatInterval : undefined; + public setUserIdentity(userInfo: UserInfo): void; + public setUserIdentity(identity: string): void; + public setUserIdentity(identity: string, name: string): void; + public setUserIdentity(userInfoOrIdentity: UserInfo | string, name?: string): void { const userInfo: UserInfo = typeof userInfoOrIdentity !== "string" ? userInfoOrIdentity : { identity: userInfoOrIdentity, name }; - const interval: number = typeof nameOrHeartbeatInterval === "number" ? nameOrHeartbeatInterval : heartbeatInterval; - const plugin = new HeartbeatPlugin(interval); - const shouldRemove: boolean = !userInfo || (!userInfo.identity && !userInfo.name); if (shouldRemove) { - this.removePlugin(plugin) delete this.defaultData[KnownEventDataKeys.UserInfo]; } else { - this.addPlugin(plugin) this.defaultData[KnownEventDataKeys.UserInfo] = userInfo; } - this.services.log.info(`user identity: ${shouldRemove ? "null" : userInfo.identity} (heartbeat interval: ${interval}ms)`); + this.services.log.info(`user identity: ${shouldRemove ? "null" : userInfo.identity}`); } /** @@ -477,7 +470,39 @@ export class Configuration { * This setting only works in environments that supports persisted storage. * There is also a performance penalty of extra IO/serialization. */ - public usePersistedQueueStorage = false; + public usePersistedQueueStorage: boolean = false; + + /** + * Gets or sets a value indicating whether to automatically send session start, + * session heartbeats and session end events. + */ + public sessionsEnabled = false; + + /** + * Internal property used to track the current session identifier. + */ + public currentSessionIdentifier: string | null = null; + + /** + * + * @param sendHeartbeats Controls whether heartbeat events are sent on an interval. + * @param heartbeatInterval The interval at which heartbeats are sent after the last sent event. The default is 1 minutes. + * @param useSessionIdManagement Allows you to manually control the session id. This is only recommended for single user desktop environments. + */ + public useSessions(sendHeartbeats: boolean = true, heartbeatInterval: number = 60000, useSessionIdManagement: boolean = false) { + this.sessionsEnabled = true; + + if (useSessionIdManagement) { + this.addPlugin(new SessionIdManagementPlugin()); + } + + const plugin = new HeartbeatPlugin(heartbeatInterval); + if (sendHeartbeats) { + this.addPlugin(plugin); + } else { + this.removePlugin(plugin); + } + } private originalSettings?: Record; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2e88cf61..7773c1b8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,7 @@ export type { ILog } from "./logging/ILog.js"; export { ConsoleLog } from "./logging/ConsoleLog.js"; export { NullLog } from "./logging/NullLog.js"; -export type { Event, IEventData } from "./models/Event.js"; +export type { Event, EventType, IEventData } from "./models/Event.js"; export { KnownEventDataKeys } from "./models/Event.js"; export type { EnvironmentInfo } from "./models/data/EnvironmentInfo.js"; export type { ManualStackingInfo } from "./models/data/ManualStackingInfo.js"; @@ -30,6 +30,7 @@ export { DuplicateCheckerPlugin } from "./plugins/default/DuplicateCheckerPlugin export { EventExclusionPlugin } from "./plugins/default/EventExclusionPlugin.js"; export { HeartbeatPlugin } from "./plugins/default/HeartbeatPlugin.js"; export { ReferenceIdPlugin } from "./plugins/default/ReferenceIdPlugin.js"; +export { SessionIdManagementPlugin } from "./plugins/default/SessionIdManagementPlugin.js"; export { IgnoredErrorProperties, SimpleErrorPlugin } from "./plugins/default/SimpleErrorPlugin.js" export { SubmissionMethodPlugin } from "./plugins/default/SubmissionMethodPlugin.js"; export { EventContext } from "./models/EventContext.js"; diff --git a/packages/core/src/models/Event.ts b/packages/core/src/models/Event.ts index a6ee2a1a..408f513d 100644 --- a/packages/core/src/models/Event.ts +++ b/packages/core/src/models/Event.ts @@ -5,9 +5,11 @@ import { UserInfo } from "./data/UserInfo.js"; import { UserDescription } from "./data/UserDescription.js"; import { ManualStackingInfo } from "./data/ManualStackingInfo.js"; +export type EventType = "error" | "usage" | "log" | "404" | "session" | string; + export interface Event { /** The event type (ie. error, log message, feature usage). */ - type?: string; + type?: EventType; /** The event source (ie. machine name, log name, feature name). */ source?: string; /** The date that the event occurred on. */ diff --git a/packages/core/src/plugins/default/HeartbeatPlugin.ts b/packages/core/src/plugins/default/HeartbeatPlugin.ts index 1a786a3e..46a7c279 100644 --- a/packages/core/src/plugins/default/HeartbeatPlugin.ts +++ b/packages/core/src/plugins/default/HeartbeatPlugin.ts @@ -9,7 +9,7 @@ export class HeartbeatPlugin implements IEventPlugin { private _interval: number; private _intervalId: ReturnType | undefined; - constructor(heartbeatInterval: number = 30000) { + constructor(heartbeatInterval: number = 60000) { this._interval = heartbeatInterval >= 30000 ? heartbeatInterval : 60000; } @@ -34,11 +34,20 @@ export class HeartbeatPlugin implements IEventPlugin { clearInterval(this._intervalId); this._intervalId = undefined; - const user = context.event.data?.[KnownEventDataKeys.UserInfo]; - if (user?.identity) { + const { config } = context.client; + if (!config.currentSessionIdentifier) { + const user = context.event.data?.[KnownEventDataKeys.UserInfo]; + if (!user?.identity) { + return Promise.resolve(); + } + + config.currentSessionIdentifier = user.identity; + } + + if (config.currentSessionIdentifier) { this._intervalId = setInterval( - () => void context.client.submitSessionHeartbeat(user.identity), - this._interval, + () => void context.client.submitSessionHeartbeat(config.currentSessionIdentifier), + this._interval ); } diff --git a/packages/core/src/plugins/default/ReferenceIdPlugin.ts b/packages/core/src/plugins/default/ReferenceIdPlugin.ts index bffc9273..d8c2f14f 100644 --- a/packages/core/src/plugins/default/ReferenceIdPlugin.ts +++ b/packages/core/src/plugins/default/ReferenceIdPlugin.ts @@ -9,7 +9,7 @@ export class ReferenceIdPlugin implements IEventPlugin { public run(context: EventPluginContext): Promise { if (!context.event.reference_id && context.event.type === "error") { // PERF: Optimize identifier creation. - context.event.reference_id = guid().replace("-", "").substring(0, 10); + context.event.reference_id = guid().replaceAll("-", "").substring(0, 10); } return Promise.resolve(); diff --git a/packages/core/src/plugins/default/SessionIdManagementPlugin.ts b/packages/core/src/plugins/default/SessionIdManagementPlugin.ts new file mode 100644 index 00000000..09cde73f --- /dev/null +++ b/packages/core/src/plugins/default/SessionIdManagementPlugin.ts @@ -0,0 +1,29 @@ +import { guid } from "../../Utils.js"; +import { EventPluginContext } from "../EventPluginContext.js"; +import { IEventPlugin } from "../IEventPlugin.js"; + +export class SessionIdManagementPlugin implements IEventPlugin { + public priority = 25; + public name = "SessionIdManagementPlugin"; + + public run(context: EventPluginContext): Promise { + const ev = context.event; + const isSessionStart: boolean = ev.type === "session"; + const { config } = context.client; + if (isSessionStart || !config.currentSessionIdentifier) { + config.currentSessionIdentifier = guid().replaceAll("-", ""); + } + + if (isSessionStart) { + ev.reference_id = config.currentSessionIdentifier; + } else { + if (!ev.data) { + ev.data = {}; + } + + ev.data["@ref:session"] = config.currentSessionIdentifier; + } + + return Promise.resolve(); + } +} diff --git a/packages/core/test/plugins/default/EventExclusionPlugin.test.ts b/packages/core/test/plugins/default/EventExclusionPlugin.test.ts index b3b681b9..5feb59b3 100644 --- a/packages/core/test/plugins/default/EventExclusionPlugin.test.ts +++ b/packages/core/test/plugins/default/EventExclusionPlugin.test.ts @@ -2,7 +2,7 @@ import { describe, test } from "@jest/globals"; import { expect } from "expect"; import { ExceptionlessClient } from "../../../src/ExceptionlessClient.js"; -import { Event, KnownEventDataKeys } from "../../../src/models/Event.js"; +import { Event, EventType, KnownEventDataKeys } from "../../../src/models/Event.js"; import { InnerErrorInfo } from "../../../src/models/data/ErrorInfo.js"; import { EventExclusionPlugin } from "../../../src/plugins/default/EventExclusionPlugin.js"; import { EventPluginContext } from "../../../src/plugins/EventPluginContext.js"; @@ -142,7 +142,7 @@ describe("EventExclusionPlugin", () => { }); describe("should exclude source type", () => { - const run = async (type: string | null | undefined, source: string | undefined, settingKey: string | null | undefined, settingValue: string | null | undefined): Promise => { + const run = async (type: EventType | null | undefined, source: string | undefined, settingKey: string | null | undefined, settingValue: string | null | undefined): Promise => { const client = new ExceptionlessClient(); if (typeof settingKey === "string") { diff --git a/packages/core/test/submission/TestSubmissionClient.test.ts b/packages/core/test/submission/TestSubmissionClient.test.ts index 49f3c0aa..af8283a4 100644 --- a/packages/core/test/submission/TestSubmissionClient.test.ts +++ b/packages/core/test/submission/TestSubmissionClient.test.ts @@ -20,7 +20,7 @@ describe("TestSubmissionClient", () => { const apiFetchMock = jest.fn<(url: string, options: FetchOptions) => Promise>>() .mockReturnValueOnce(Promise.resolve(new Response(202, "", NaN, NaN, undefined))); - const events = [{ type: "log", message: "From js client", reference_id: "123454321" }]; + const events: Event[] = [{ type: "log", message: "From js client", reference_id: "123454321" }]; const client = new TestSubmissionClient(config, apiFetchMock); await client.submitEvents(events); expect(apiFetchMock).toHaveBeenCalledTimes(1); diff --git a/packages/node/src/plugins/NodeLifeCyclePlugin.ts b/packages/node/src/plugins/NodeLifeCyclePlugin.ts index 0ed02505..298293cc 100644 --- a/packages/node/src/plugins/NodeLifeCyclePlugin.ts +++ b/packages/node/src/plugins/NodeLifeCyclePlugin.ts @@ -23,6 +23,10 @@ export class NodeLifeCyclePlugin implements IEventPlugin { void this._client?.submitLog("beforeExit", message, "Error"); } + if (this._client?.config.sessionsEnabled) { + void this._client?.submitSessionEnd(); + } + void this._client?.suspend(); // Application will now exit. }); diff --git a/packages/react/package.json b/packages/react/package.json index a142dd6b..dc54e391 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -33,7 +33,7 @@ "./package.json": "./package.json" }, "scripts": { - "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", + "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", "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" }, "sideEffects": false, diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 2f16891f..c357c742 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": ["DOM", "ES2020"], + "lib": [ + "DOM", + "ES2021" + ], "outDir": "dist", "rootDir": "src", "jsx": "react", diff --git a/packages/vue/package.json b/packages/vue/package.json index 4b72d763..b38258e7 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -33,7 +33,7 @@ "./package.json": "./package.json" }, "scripts": { - "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", + "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", "watch": "tsc -p tsconfig.json -w --preserveWatchOutput & && esbuild src/index.ts --bundle --sourcemap --target=es2017 --format=esm --watch --outfile=dist/index.bundle.js &" }, "sideEffects": false, diff --git a/packages/vue/tsconfig.json b/packages/vue/tsconfig.json index ca546457..db5dd490 100644 --- a/packages/vue/tsconfig.json +++ b/packages/vue/tsconfig.json @@ -1,7 +1,10 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "lib": ["DOM", "ES2020"], + "lib": [ + "DOM", + "ES2021" + ], "outDir": "dist", "rootDir": "src" }, diff --git a/tsconfig.json b/tsconfig.json index dec5f010..c41ce413 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "forceConsistentCasingInFileNames": true, "isolatedModules": true, "lib": [ - "ES2020", + "ES2021", "DOM" ], "module": "ESNext", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy