From 87a7550e31e04185d389b67c685f9b5f8b1b6daa Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 12:21:05 -0400 Subject: [PATCH 01/11] Event driven session pushing --- .../src/client/constants/sessions.ts | 8 ++++- sdk/highlight-run/src/sdk/record.ts | 29 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index eef6d5174..51888d16e 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -6,7 +6,13 @@ export const FIRST_SEND_FREQUENCY = 1000 * The amount of time between sending the client-side payload to Highlight backend client. * In milliseconds. */ -export const SEND_FREQUENCY = 1000 * 2 +export const SEND_FREQUENCY = 1000 * 15 + +/** + * Payload size threshold for triggering sends. + * In bytes. + */ +export const PAYLOAD_SIZE_THRESHOLD = 32e5 // 32MB /** * Maximum length of a session diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index 83d4c7309..70886ff93 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -30,6 +30,7 @@ import { LAUNCHDARKLY_PATH_PREFIX, LAUNCHDARKLY_URL, MAX_SESSION_LENGTH, + PAYLOAD_SIZE_THRESHOLD, SEND_FREQUENCY, SNAPSHOT_SETTINGS, VISIBILITY_DEBOUNCE_MS, @@ -581,6 +582,8 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.logger.log('received isCheckout emit', { event }) } this.events.push(event) + // Check if we should send payload early based on size + this._checkForImmediateSave() } emit.bind(this) @@ -1159,6 +1162,32 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this._lastSnapshotTime = new Date().getTime() } + private _estimatePayloadSize(): number { + if (this.events.length === 0) { + return 0 + } + // Rough estimation: average event size is ~1KB + const averageEventSize = 1024 + return this.events.length * averageEventSize + } + + // Flush data if size threshold is exceeded + private _checkForImmediateSave(): void { + if (this.state !== 'Recording' || !this.pushPayloadTimerId) { + return + } + + const estimatedSize = this._estimatePayloadSize() + + if (estimatedSize >= PAYLOAD_SIZE_THRESHOLD) { + this.logger.log( + `Triggering immediate save due to large payload size (${estimatedSize} bytes)`, + ) + + this._save() + } + } + register(client: LDClient, metadata: LDPluginEnvironmentMetadata) { this._integrations.push(new LaunchDarklyIntegration(client, metadata)) } From c20650bb3709f437259f80f24ddc14a2dd675c7b Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 12:22:57 -0400 Subject: [PATCH 02/11] Set to 32 kb --- sdk/highlight-run/src/client/constants/sessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index 51888d16e..e413f9548 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -12,7 +12,7 @@ export const SEND_FREQUENCY = 1000 * 15 * Payload size threshold for triggering sends. * In bytes. */ -export const PAYLOAD_SIZE_THRESHOLD = 32e5 // 32MB +export const PAYLOAD_SIZE_THRESHOLD = 32e3 // 32KB /** * Maximum length of a session From 73bee02f7100031f9ebcfc804e5940cda151182f Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 12:53:43 -0400 Subject: [PATCH 03/11] Improvementments --- e2e/aws-lambda/.aws-sam/build.toml | 4 ++-- sdk/highlight-run/src/client/constants/sessions.ts | 2 +- sdk/highlight-run/src/sdk/record.ts | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/e2e/aws-lambda/.aws-sam/build.toml b/e2e/aws-lambda/.aws-sam/build.toml index a67218894..f61cc936d 100644 --- a/e2e/aws-lambda/.aws-sam/build.toml +++ b/e2e/aws-lambda/.aws-sam/build.toml @@ -1,7 +1,7 @@ # This file is auto generated by SAM CLI build command -[function_build_definitions.02e7f5fb-ae39-4d32-a885-1919db9b363a] -codeuri = "/Users/vkorolik/work/observability/observability-sdk/e2e/aws-lambda" +[function_build_definitions.8f6a69df-7402-493a-b9a2-b7daab39e8cc] +codeuri = "/Users/spenceramarantides/code/launchdarkly/observability/observability-sdk/e2e/aws-lambda" runtime = "nodejs20.x" architecture = "x86_64" handler = "src/handlers/api.handler" diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index e413f9548..12dabfe54 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -12,7 +12,7 @@ export const SEND_FREQUENCY = 1000 * 15 * Payload size threshold for triggering sends. * In bytes. */ -export const PAYLOAD_SIZE_THRESHOLD = 32e3 // 32KB +export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 2e5 // 200KB /** * Maximum length of a session diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index 70886ff93..b1129fd2a 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -30,7 +30,7 @@ import { LAUNCHDARKLY_PATH_PREFIX, LAUNCHDARKLY_URL, MAX_SESSION_LENGTH, - PAYLOAD_SIZE_THRESHOLD, + UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD, SEND_FREQUENCY, SNAPSHOT_SETTINGS, VISIBILITY_DEBOUNCE_MS, @@ -1038,6 +1038,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.pushPayloadTimerId = undefined } this.pushPayloadTimerId = setTimeout(() => { + this.logger.log(`Triggering immediate save due to timeout`) this._save() }, SEND_FREQUENCY) } @@ -1179,7 +1180,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, const estimatedSize = this._estimatePayloadSize() - if (estimatedSize >= PAYLOAD_SIZE_THRESHOLD) { + if (estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD) { this.logger.log( `Triggering immediate save due to large payload size (${estimatedSize} bytes)`, ) From f5f037f8035c891d11c5fc7190609f59927fcd95 Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 13:17:29 -0400 Subject: [PATCH 04/11] Don't save more than once --- e2e/aws-lambda/.aws-sam/build.toml | 4 ++-- sdk/highlight-run/src/sdk/record.ts | 18 +++++++++++++++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/aws-lambda/.aws-sam/build.toml b/e2e/aws-lambda/.aws-sam/build.toml index f61cc936d..a67218894 100644 --- a/e2e/aws-lambda/.aws-sam/build.toml +++ b/e2e/aws-lambda/.aws-sam/build.toml @@ -1,7 +1,7 @@ # This file is auto generated by SAM CLI build command -[function_build_definitions.8f6a69df-7402-493a-b9a2-b7daab39e8cc] -codeuri = "/Users/spenceramarantides/code/launchdarkly/observability/observability-sdk/e2e/aws-lambda" +[function_build_definitions.02e7f5fb-ae39-4d32-a885-1919db9b363a] +codeuri = "/Users/vkorolik/work/observability/observability-sdk/e2e/aws-lambda" runtime = "nodejs20.x" architecture = "x86_64" handler = "src/handlers/api.handler" diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index b1129fd2a..492e9a3a9 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -143,6 +143,7 @@ export class RecordSDK implements Record { hasSessionUnloaded!: boolean hasPushedData!: boolean reloaded!: boolean + saving!: boolean _hasPreviouslyInitialized!: boolean _recordStop!: listenerHandler | undefined _integrations: IntegrationClient[] = [] @@ -337,6 +338,7 @@ export class RecordSDK implements Record { this.events = [] this.hasSessionUnloaded = false this.hasPushedData = false + this.saving = false if (window.Intercom) { window.Intercom('onShow', () => { @@ -582,8 +584,11 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.logger.log('received isCheckout emit', { event }) } this.events.push(event) + // Check if we should send payload early based on size - this._checkForImmediateSave() + if (!this.saving) { + this._checkForImmediateSave() + } } emit.bind(this) @@ -990,6 +995,11 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, // Reset the events array and push to a backend. async _save() { + if (this.saving) { + return + } + this.saving = true + try { if ( this.state === 'Recording' && @@ -1038,10 +1048,12 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.pushPayloadTimerId = undefined } this.pushPayloadTimerId = setTimeout(() => { - this.logger.log(`Triggering immediate save due to timeout`) + this.logger.log(`Triggering save due to timeout`) this._save() }, SEND_FREQUENCY) } + + this.saving = false } /** @@ -1182,7 +1194,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, if (estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD) { this.logger.log( - `Triggering immediate save due to large payload size (${estimatedSize} bytes)`, + `Triggering save due to large payload size (${estimatedSize} bytes)`, ) this._save() From 2085ab4fbf76adedeacb5af0f2311b1bcae51abf Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 13:18:34 -0400 Subject: [PATCH 05/11] Break down constant --- sdk/highlight-run/src/client/constants/sessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index 12dabfe54..dd3b9a53f 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -12,7 +12,7 @@ export const SEND_FREQUENCY = 1000 * 15 * Payload size threshold for triggering sends. * In bytes. */ -export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 2e5 // 200KB +export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 1024 * 200 // 200KB /** * Maximum length of a session From 186660f26c91eec1c7e9e554162b4f258571e24d Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 13:38:31 -0400 Subject: [PATCH 06/11] Make functions more understandable --- sdk/highlight-run/src/sdk/record.ts | 35 +++++++++-------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index 492e9a3a9..e0dabfab0 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -586,8 +586,9 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.events.push(event) // Check if we should send payload early based on size - if (!this.saving) { - this._checkForImmediateSave() + if (!this.saving && this._checkForImmediateSave()) { + this.logger.log('Triggering save due to large payload size') + this._save() } } emit.bind(this) @@ -1124,7 +1125,9 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, highlight_logs: highlightLogs || undefined, } - const { compressedBase64 } = await payloadToBase64(sessionPayload) + const { compressedBase64, compressedSize } = + await payloadToBase64(sessionPayload) + this.logger.log(`Compressed payload size: ${compressedSize} bytes`) await sendFn({ session_secure_id: this.sessionData.sessionSecureID, payload_id: payloadId.toString(), @@ -1174,31 +1177,15 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this._eventBytesSinceSnapshot = 0 this._lastSnapshotTime = new Date().getTime() } - - private _estimatePayloadSize(): number { - if (this.events.length === 0) { - return 0 - } - // Rough estimation: average event size is ~1KB - const averageEventSize = 1024 - return this.events.length * averageEventSize - } - // Flush data if size threshold is exceeded - private _checkForImmediateSave(): void { + private _checkForImmediateSave(): boolean { if (this.state !== 'Recording' || !this.pushPayloadTimerId) { - return + return false } - const estimatedSize = this._estimatePayloadSize() - - if (estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD) { - this.logger.log( - `Triggering save due to large payload size (${estimatedSize} bytes)`, - ) - - this._save() - } + // Rough estimation: average event size is ~1KB + const estimatedSize = this.events.length * 1024 + return estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD } register(client: LDClient, metadata: LDPluginEnvironmentMetadata) { From 7d84cc2ec22c844fb219804fd36ff29dcefd7fcd Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 15:59:53 -0400 Subject: [PATCH 07/11] WIP for sending beacon --- sdk/highlight-run/src/sdk/record.ts | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index e0dabfab0..e9aaec7ed 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -889,6 +889,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, clearTimeout(this.pushPayloadTimerId) this.pushPayloadTimerId = undefined } + this._saveOnUnload() } window.addEventListener('beforeunload', unloadListener) this.listeners.push(() => @@ -902,6 +903,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.addCustomEvent('Page Unload', '') setSessionSecureID(this.sessionData.sessionSecureID) setSessionData(this.sessionData) + this._saveOnUnload() } window.addEventListener('beforeunload', unloadListener) this.listeners.push(() => @@ -917,10 +919,11 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.addCustomEvent('Page Unload', '') setSessionSecureID(this.sessionData.sessionSecureID) setSessionData(this.sessionData) + this._saveOnUnload() } window.addEventListener('pagehide', unloadListener) this.listeners.push(() => - window.removeEventListener('beforeunload', unloadListener), + window.removeEventListener('pagehide', unloadListener), ) } } @@ -1188,6 +1191,42 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, return estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD } + private _saveWithBeacon( + payload: PushSessionEventsMutationVariables, + ): Promise { + try { + let blob = new Blob( + [ + JSON.stringify({ + query: print(PushSessionEventsDocument), + variables: payload, + }), + ], + { + type: 'application/json', + }, + ) + const success = navigator.sendBeacon(`${this._backendUrl}`, blob) + this.logger.log(`Beacon send ${success ? 'succeeded' : 'failed'}`) + } catch (error) { + this.logger.log('Beacon API failed', error) + } + + return Promise.resolve(0) + } + + private _saveOnUnload(): void { + if (this.events.length === 0) { + return + } + + try { + this._sendPayload({ sendFn: this._saveWithBeacon }) + } catch (error) { + this.logger.log('Failed to save session data on unload:', error) + } + } + register(client: LDClient, metadata: LDPluginEnvironmentMetadata) { this._integrations.push(new LaunchDarklyIntegration(client, metadata)) } From db6dbdd400d10d0c97f62d67caa67b1a45b00114 Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 17:45:59 -0400 Subject: [PATCH 08/11] Send request on unload --- sdk/highlight-run/src/sdk/record.ts | 51 +++++++++++++++-------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index e9aaec7ed..7ed5bb317 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -38,7 +38,6 @@ import { import { ReplayEventsInput } from '../client/graph/generated/schemas' import { ClickListener } from '../client/listeners/click-listener/click-listener' import { FocusListener } from '../client/listeners/focus-listener/focus-listener' -import { PageVisibilityListener } from '../client/listeners/page-visibility-listener' import { SegmentIntegrationListener } from '../client/listeners/segment-integration-listener' import SessionShortcutListener from '../client/listeners/session-shortcut/session-shortcut-listener' import { @@ -713,6 +712,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.addCustomEvent('TabHidden', false) } else { this.addCustomEvent('TabHidden', true) + this._saveOnUnload() if (this.options.disableBackgroundRecording) { this.stop() } @@ -874,8 +874,10 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } else { // Send the payload every time the page is no longer visible - this includes when the tab is closed, as well // as when switching tabs or apps on mobile. Non-blocking. - PageVisibilityListener((isTabHidden) => - this._visibilityHandler(isTabHidden), + document.addEventListener('visibilitychange', () => + this._visibilityHandler( + document.visibilityState === 'hidden', + ), ) this.logger.log('Set up document visibility listener.') } @@ -889,7 +891,6 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, clearTimeout(this.pushPayloadTimerId) this.pushPayloadTimerId = undefined } - this._saveOnUnload() } window.addEventListener('beforeunload', unloadListener) this.listeners.push(() => @@ -903,7 +904,6 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.addCustomEvent('Page Unload', '') setSessionSecureID(this.sessionData.sessionSecureID) setSessionData(this.sessionData) - this._saveOnUnload() } window.addEventListener('beforeunload', unloadListener) this.listeners.push(() => @@ -919,7 +919,6 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.addCustomEvent('Page Unload', '') setSessionSecureID(this.sessionData.sessionSecureID) setSessionData(this.sessionData) - this._saveOnUnload() } window.addEventListener('pagehide', unloadListener) this.listeners.push(() => @@ -1192,26 +1191,25 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } private _saveWithBeacon( + backendUrl: string, payload: PushSessionEventsMutationVariables, ): Promise { - try { - let blob = new Blob( - [ - JSON.stringify({ - query: print(PushSessionEventsDocument), - variables: payload, - }), - ], - { - type: 'application/json', - }, - ) - const success = navigator.sendBeacon(`${this._backendUrl}`, blob) - this.logger.log(`Beacon send ${success ? 'succeeded' : 'failed'}`) - } catch (error) { - this.logger.log('Beacon API failed', error) - } - + let blob = new Blob( + [ + JSON.stringify({ + query: print(PushSessionEventsDocument), + variables: payload, + }), + ], + { + type: 'application/json', + }, + ) + window.fetch(`${backendUrl}`, { + method: 'POST', + body: blob, + keepalive: true, + }) return Promise.resolve(0) } @@ -1221,7 +1219,10 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } try { - this._sendPayload({ sendFn: this._saveWithBeacon }) + this._sendPayload({ + sendFn: (payload) => + this._saveWithBeacon(this._backendUrl, payload), + }) } catch (error) { this.logger.log('Failed to save session data on unload:', error) } From 1f9a788b57e8e722f856b593e27afcd10d33b257 Mon Sep 17 00:00:00 2001 From: SpennyNDaJets Date: Thu, 7 Aug 2025 17:52:11 -0400 Subject: [PATCH 09/11] Use handler --- .../listeners/page-visibility-listener.tsx | 35 ++----------------- sdk/highlight-run/src/sdk/record.ts | 7 ++-- 2 files changed, 6 insertions(+), 36 deletions(-) diff --git a/sdk/highlight-run/src/client/listeners/page-visibility-listener.tsx b/sdk/highlight-run/src/client/listeners/page-visibility-listener.tsx index 3762204f0..f3d38ea23 100644 --- a/sdk/highlight-run/src/client/listeners/page-visibility-listener.tsx +++ b/sdk/highlight-run/src/client/listeners/page-visibility-listener.tsx @@ -1,34 +1,8 @@ export const PageVisibilityListener = ( callback: (isTabHidden: boolean) => void, ) => { - let hidden: string | undefined = undefined - let visibilityChangeEventName: string | undefined = undefined - - if (typeof document.hidden !== 'undefined') { - // Opera 12.10 and Firefox 18 and later support - hidden = 'hidden' - visibilityChangeEventName = 'visibilitychange' - // @ts-expect-error - } else if (typeof document.msHidden !== 'undefined') { - hidden = 'msHidden' - visibilityChangeEventName = 'msvisibilitychange' - // @ts-expect-error - } else if (typeof document.webkitHidden !== 'undefined') { - hidden = 'webkitHidden' - visibilityChangeEventName = 'webkitvisibilitychange' - } - - if (visibilityChangeEventName === undefined) { - return () => {} - } - if (hidden === undefined) { - return () => {} - } - - const hiddenPropertyName = hidden const listener = () => { - // @ts-expect-error - const tabState = document[hiddenPropertyName] + const tabState = document.hidden if (tabState) { callback(true) @@ -37,9 +11,6 @@ export const PageVisibilityListener = ( } } - document.addEventListener(visibilityChangeEventName, listener) - - const eventNameToRemove = visibilityChangeEventName - - return () => document.removeEventListener(eventNameToRemove, listener) + document.addEventListener('visibilitychange', listener) + return () => document.removeEventListener('visibilitychange', listener) } diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index 7ed5bb317..e9b543998 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -38,6 +38,7 @@ import { import { ReplayEventsInput } from '../client/graph/generated/schemas' import { ClickListener } from '../client/listeners/click-listener/click-listener' import { FocusListener } from '../client/listeners/focus-listener/focus-listener' +import { PageVisibilityListener } from '../client/listeners/page-visibility-listener' import { SegmentIntegrationListener } from '../client/listeners/segment-integration-listener' import SessionShortcutListener from '../client/listeners/session-shortcut/session-shortcut-listener' import { @@ -874,10 +875,8 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } else { // Send the payload every time the page is no longer visible - this includes when the tab is closed, as well // as when switching tabs or apps on mobile. Non-blocking. - document.addEventListener('visibilitychange', () => - this._visibilityHandler( - document.visibilityState === 'hidden', - ), + PageVisibilityListener((isTabHidden) => + this._visibilityHandler(isTabHidden), ) this.logger.log('Set up document visibility listener.') } From 9d6b2c426a76f0dee29bdf92a3b3e1fb1960a8f9 Mon Sep 17 00:00:00 2001 From: Nicholas Tiner Date: Thu, 7 Aug 2025 17:08:48 -0700 Subject: [PATCH 10/11] Added more accurate estimated size checks, lowered byte size threshold for easier testing/demo --- .../src/client/constants/sessions.ts | 3 +- sdk/highlight-run/src/sdk/record.ts | 46 ++++++++++++------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index dd3b9a53f..26b83d868 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -12,7 +12,8 @@ export const SEND_FREQUENCY = 1000 * 15 * Payload size threshold for triggering sends. * In bytes. */ -export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 1024 * 200 // 200KB +// TODO: This is a temporary low value for testing. +export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 1024 * 5 // 5KB /** * Maximum length of a session diff --git a/sdk/highlight-run/src/sdk/record.ts b/sdk/highlight-run/src/sdk/record.ts index e9b543998..d5feb367f 100644 --- a/sdk/highlight-run/src/sdk/record.ts +++ b/sdk/highlight-run/src/sdk/record.ts @@ -87,6 +87,7 @@ import type { Hook, LDClient } from '../integrations/launchdarkly' import { LaunchDarklyIntegration } from '../integrations/launchdarkly' import { LDPluginEnvironmentMetadata } from '../plugins/plugin' import { RecordOptions } from '../client/types/record' +import { strToU8 } from 'fflate' interface HighlightWindow extends Window { Highlight: Highlight @@ -144,6 +145,7 @@ export class RecordSDK implements Record { hasPushedData!: boolean reloaded!: boolean saving!: boolean + _estimatedEventsByteSize!: number _hasPreviouslyInitialized!: boolean _recordStop!: listenerHandler | undefined _integrations: IntegrationClient[] = [] @@ -339,6 +341,7 @@ export class RecordSDK implements Record { this.hasSessionUnloaded = false this.hasPushedData = false this.saving = false + this._estimatedEventsByteSize = 0 if (window.Intercom) { window.Intercom('onShow', () => { @@ -586,7 +589,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this.events.push(event) // Check if we should send payload early based on size - if (!this.saving && this._checkForImmediateSave()) { + if (!this.saving && this._checkForImmediateSave(event)) { this.logger.log('Triggering save due to large payload size') this._save() } @@ -1016,6 +1019,9 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, }) await this._reset({}) } + + const eventsToSend = this._captureAndResetEventsState() + let sendFn = undefined if (this.options?.sendMode === 'local') { sendFn = async (payload: any) => { @@ -1037,7 +1043,7 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, return 0 } } - await this._sendPayload({ sendFn }) + await this._sendPayload({ sendFn, events: eventsToSend }) this.hasPushedData = true this.sessionData.lastPushTime = Date.now() setSessionData(this.sessionData) @@ -1084,13 +1090,13 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, async _sendPayload({ sendFn, + events, }: { sendFn?: ( payload: PushSessionEventsMutationVariables, ) => Promise + events: eventWithTime[] }) { - const events = [...this.events] - // if it is time to take a full snapshot, // ensure the snapshot is at the beginning of the next payload // After snapshot thresholds have been met, @@ -1153,15 +1159,6 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } setSessionData(this.sessionData) - // We are creating a weak copy of the events. rrweb could have pushed more events to this.events while we send the request with the events as a payload. - // Originally, we would clear this.events but this could lead to a race condition. - // Example Scenario: - // 1. Create the events payload from this.events (with N events) - // 2. rrweb pushes to this.events (with M events) - // 3. Network request made to push payload (Only includes N events) - // 4. this.events is cleared (we lose M events) - this.events = this.events.slice(events.length) - clearHighlightLogs(highlightLogs) } @@ -1178,15 +1175,18 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, this._eventBytesSinceSnapshot = 0 this._lastSnapshotTime = new Date().getTime() } + // Flush data if size threshold is exceeded - private _checkForImmediateSave(): boolean { + private _checkForImmediateSave(newEvent: eventWithTime): boolean { if (this.state !== 'Recording' || !this.pushPayloadTimerId) { return false } - // Rough estimation: average event size is ~1KB - const estimatedSize = this.events.length * 1024 - return estimatedSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD + const newEventByteSize = strToU8(JSON.stringify(newEvent)).length + this._estimatedEventsByteSize += newEventByteSize + return ( + this._estimatedEventsByteSize >= UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD + ) } private _saveWithBeacon( @@ -1213,20 +1213,32 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`, } private _saveOnUnload(): void { + console.log('saving on unload', this.events.length) if (this.events.length === 0) { return } try { + const eventsToSend = this._captureAndResetEventsState() + this._sendPayload({ sendFn: (payload) => this._saveWithBeacon(this._backendUrl, payload), + events: eventsToSend, }) } catch (error) { this.logger.log('Failed to save session data on unload:', error) } } + // Atomic capture and reset events state synchronously to avoid race conditions + private _captureAndResetEventsState(): eventWithTime[] { + const eventsToSend = [...this.events] + this.events = [] + this._estimatedEventsByteSize = 0 + return eventsToSend + } + register(client: LDClient, metadata: LDPluginEnvironmentMetadata) { this._integrations.push(new LaunchDarklyIntegration(client, metadata)) } From b318ce49721e31319ffb24240685c60db0dc470d Mon Sep 17 00:00:00 2001 From: Nicholas Tiner Date: Thu, 7 Aug 2025 17:09:52 -0700 Subject: [PATCH 11/11] Missed commit a file --- sdk/highlight-run/src/client/constants/sessions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/highlight-run/src/client/constants/sessions.ts b/sdk/highlight-run/src/client/constants/sessions.ts index 26b83d868..6932b7c4a 100644 --- a/sdk/highlight-run/src/client/constants/sessions.ts +++ b/sdk/highlight-run/src/client/constants/sessions.ts @@ -13,7 +13,7 @@ export const SEND_FREQUENCY = 1000 * 15 * In bytes. */ // TODO: This is a temporary low value for testing. -export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 1024 * 5 // 5KB +export const UNCOMPRESSED_PAYLOAD_SIZE_THRESHOLD = 1024 * 10 // 10KB /** * Maximum length of a session