From d1ad80be164057408f9f848cc6843ef4abcb170a Mon Sep 17 00:00:00 2001 From: Robbie Date: Wed, 19 Jun 2024 10:06:41 +0100 Subject: [PATCH 01/17] feat: Allow bootstrapping session id (#1251) * Allow bootstrapping session id * Add test for bootstrapping session id --- src/__tests__/sessionid.ts | 21 +++++++++++++++++++-- src/__tests__/test-uuid.test.ts | 20 ++++++++++++++++++-- src/sessionid.ts | 11 ++++++++++- src/types.ts | 8 ++++++++ src/uuidv7.ts | 14 ++++++++++++++ 5 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/__tests__/sessionid.ts b/src/__tests__/sessionid.ts index 077679494..34e336424 100644 --- a/src/__tests__/sessionid.ts +++ b/src/__tests__/sessionid.ts @@ -1,8 +1,8 @@ import { SessionIdManager } from '../sessionid' import { SESSION_ID } from '../constants' import { sessionStore } from '../storage' -import { uuidv7 } from '../uuidv7' -import { PostHogConfig, Properties } from '../types' +import { uuidv7, uuid7ToTimestampMs } from '../uuidv7' +import { BootstrapConfig, PostHogConfig, Properties } from '../types' import { PostHogPersistence } from '../posthog-persistence' import { assignableWindow } from '../utils/globals' @@ -38,6 +38,7 @@ describe('Session ID manager', () => { ;(sessionStore.is_supported as jest.Mock).mockReturnValue(true) jest.spyOn(global, 'Date').mockImplementation(() => new originalDate(now)) ;(uuidv7 as jest.Mock).mockReturnValue('newUUID') + ;(uuid7ToTimestampMs as jest.Mock).mockReturnValue(timestamp) }) describe('new session id manager', () => { @@ -64,6 +65,22 @@ describe('Session ID manager', () => { }) expect(sessionStore.set).toHaveBeenCalledWith('ph_persistance-name_window_id', 'newUUID') }) + + it('should allow bootstrapping of the session id', () => { + // arrange + const bootstrapSessionId = 'bootstrap-session-id' + const bootstrap: BootstrapConfig = { + sessionID: bootstrapSessionId, + } + const sessionIdManager = new SessionIdManager({ ...config, bootstrap }, persistence as PostHogPersistence) + + // act + const { sessionId, sessionStartTimestamp } = sessionIdManager.checkAndGetSessionAndWindowId(false, now) + + // assert + expect(sessionId).toEqual(bootstrapSessionId) + expect(sessionStartTimestamp).toEqual(timestamp) + }) }) describe('stored session data', () => { diff --git a/src/__tests__/test-uuid.test.ts b/src/__tests__/test-uuid.test.ts index fc884460c..da798e197 100644 --- a/src/__tests__/test-uuid.test.ts +++ b/src/__tests__/test-uuid.test.ts @@ -1,8 +1,24 @@ -import { uuidv7 } from '../uuidv7' - +import { uuid7ToTimestampMs, uuidv7 } from '../uuidv7' +const TEN_SECONDS = 10_000 describe('uuid', () => { it('should be a uuid when requested', () => { expect(uuidv7()).toHaveLength(36) expect(uuidv7()).not.toEqual(uuidv7()) }) + describe('uuid7ToTimestampMs', () => { + it('should convert a UUIDv7 generated with uuidv7() to a timestamp', () => { + const uuid = uuidv7() + const timestamp = uuid7ToTimestampMs(uuid) + const now = Date.now() + expect(typeof timestamp).toBe('number') + expect(timestamp).toBeLessThan(now + TEN_SECONDS) + expect(timestamp).toBeGreaterThan(now - TEN_SECONDS) + }) + it('should convert a known UUIDv7 to a known timestamp', () => { + const uuid = '01902c33-4925-7f20-818a-4095f9251383' + const timestamp = uuid7ToTimestampMs(uuid) + const expected = new Date('Tue, 18 Jun 2024 16:34:36.965 GMT').getTime() + expect(timestamp).toBe(expected) + }) + }) }) diff --git a/src/sessionid.ts b/src/sessionid.ts index d3e1772d7..dbc4586d4 100644 --- a/src/sessionid.ts +++ b/src/sessionid.ts @@ -2,7 +2,7 @@ import { PostHogPersistence } from './posthog-persistence' import { SESSION_ID } from './constants' import { sessionStore } from './storage' import { PostHogConfig, SessionIdChangedCallback } from './types' -import { uuidv7 } from './uuidv7' +import { uuid7ToTimestampMs, uuidv7 } from './uuidv7' import { window } from './utils/globals' import { isArray, isNumber, isUndefined } from './utils/type-utils' @@ -76,6 +76,15 @@ export class SessionIdManager { sessionStore.set(this._primary_window_exists_storage_key, true) } + if (this.config.bootstrap?.sessionID) { + try { + const sessionStartTimestamp = uuid7ToTimestampMs(this.config.bootstrap.sessionID) + this._setSessionId(this.config.bootstrap.sessionID, new Date().getTime(), sessionStartTimestamp) + } catch (e) { + logger.error('Invalid sessionID in bootstrap', e) + } + } + this._listenToReloadWindow() } diff --git a/src/types.ts b/src/types.ts index 4d736687b..4be1ae3aa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,6 +67,14 @@ export interface BootstrapConfig { isIdentifiedID?: boolean featureFlags?: Record featureFlagPayloads?: Record + /** + * Optionally provide a sessionID, this is so that you can provide an existing sessionID here to continue a user's session across a domain or device. It MUST be: + * - unique to this user + * - a valid UUID v7 + * - the timestamp part must be <= the timestamp of the first event in the session + * - the timestamp of the last event in the session must be < the timestamp part + 24 hours + * **/ + sessionID?: string } export interface PostHogConfig { diff --git a/src/uuidv7.ts b/src/uuidv7.ts index 0c6bf0785..7bcaf6c2c 100644 --- a/src/uuidv7.ts +++ b/src/uuidv7.ts @@ -252,3 +252,17 @@ export const uuidv7 = (): string => uuidv7obj().toString() /** Generates a UUIDv7 object. */ const uuidv7obj = (): UUID => (defaultGenerator || (defaultGenerator = new V7Generator())).generate() + +export const uuid7ToTimestampMs = (uuid: string): number => { + // remove hyphens + const hex = uuid.replace(/-/g, '') + // ensure that it's a version 7 UUID + if (hex.length !== 32) { + throw new Error('Not a valid UUID') + } + if (hex[12] !== '7') { + throw new Error('Not a UUIDv7') + } + // the first 6 bytes are the timestamp, which means that we can read only the first 12 hex characters + return parseInt(hex.substring(0, 12), 16) +} From a67c7298b8c06395e4bff50bbc56221e051ca131 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Thu, 20 Jun 2024 11:32:14 +0100 Subject: [PATCH 02/17] fix: no scheduled snapshots while idle (#1254) Co-authored-by: David Newell --- .../replay/sessionrecording.test.ts | 25 ++---------- src/extensions/replay/sessionrecording.ts | 40 +++++++++---------- src/types.ts | 2 + 3 files changed, 24 insertions(+), 43 deletions(-) diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index 1d481289d..ab895de2a 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -1335,7 +1335,6 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createFullSnapshot({}) as eventWithTime) - // custom event is buffered expect(sessionRecording['buffer']).toEqual({ data: [ { @@ -1361,7 +1360,6 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createMetaSnapshot({}) as eventWithTime) - // custom event is buffered expect(sessionRecording['buffer']).toEqual({ data: [ { @@ -1389,7 +1387,6 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createStyleSnapshot({}) as eventWithTime) - // custom event is buffered expect(sessionRecording['buffer']).toEqual({ data: [ { @@ -1445,11 +1442,7 @@ describe('SessionRecording', () => { // this triggers idle state and isn't a user interaction so does not take a full snapshot emitInactiveEvent(thirdActivityTimestamp, true) - expect(_addCustomEvent).toHaveBeenCalledWith('sessionIdle', { - reason: 'user inactivity', - threshold: 300000, - timeSinceLastActive: 300900, - }) + // event was not active so activity timestamp is not updated expect(sessionRecording['_lastActivityTimestamp']).toEqual(firstActivityTimestamp) expect(assignableWindow.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(1) @@ -1481,10 +1474,7 @@ describe('SessionRecording', () => { // this triggers exit from idle state _and_ is a user interaction, so we take a full snapshot const fourthSnapshot = emitActiveEvent(fourthActivityTimestamp) - expect(_addCustomEvent).toHaveBeenCalledWith('sessionNoLongerIdle', { - reason: 'user activity', - type: INCREMENTAL_SNAPSHOT_EVENT_TYPE, - }) + expect(sessionRecording['_lastActivityTimestamp']).toEqual(fourthActivityTimestamp) expect(assignableWindow.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(2) @@ -1543,11 +1533,7 @@ describe('SessionRecording', () => { // this triggers idle state and isn't a user interaction so does not take a full snapshot emitInactiveEvent(thirdActivityTimestamp, true) - expect(_addCustomEvent).toHaveBeenCalledWith('sessionIdle', { - reason: 'user inactivity', - threshold: 300000, - timeSinceLastActive: 1799901, - }) + // event was not active so activity timestamp is not updated expect(sessionRecording['_lastActivityTimestamp']).toEqual(firstActivityTimestamp) expect(assignableWindow.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(1) @@ -1583,10 +1569,7 @@ describe('SessionRecording', () => { // this triggers exit from idle state _and_ is a user interaction, so we take a full snapshot const fourthSnapshot = emitActiveEvent(fourthActivityTimestamp) - expect(_addCustomEvent).toHaveBeenCalledWith('sessionNoLongerIdle', { - reason: 'user activity', - type: INCREMENTAL_SNAPSHOT_EVENT_TYPE, - }) + expect(sessionRecording['_lastActivityTimestamp']).toEqual(fourthActivityTimestamp) expect(assignableWindow.rrweb.record.takeFullSnapshot).toHaveBeenCalledTimes(2) diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index 967602a36..d138cf921 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -212,6 +212,10 @@ export class SessionRecording { return this.instance.sessionManager } + private get fullSnapshotIntervalMillis(): number { + return this.instance.config.session_recording?.full_snapshot_interval_millis || FIVE_MINUTES + } + private get isSampled(): boolean | null { const currentValue = this.instance.get_property(SESSION_RECORDING_IS_SAMPLED) return isBoolean(currentValue) ? currentValue : null @@ -570,11 +574,6 @@ export class SessionRecording { // We check if the lastActivityTimestamp is old enough to go idle if (event.timestamp - this._lastActivityTimestamp > RECORDING_IDLE_ACTIVITY_TIMEOUT_MS) { this.isIdle = true - this._tryAddCustomEvent('sessionIdle', { - reason: 'user inactivity', - timeSinceLastActive: event.timestamp - this._lastActivityTimestamp, - threshold: RECORDING_IDLE_ACTIVITY_TIMEOUT_MS, - }) // don't take full snapshots while idle clearTimeout(this._fullSnapshotTimer) // proactively flush the buffer in case the session is idle for a long time @@ -586,7 +585,7 @@ export class SessionRecording { if (isUserInteraction) { this._lastActivityTimestamp = event.timestamp if (this.isIdle) { - // Remove the idle state if set and trigger a full snapshot as we will have ignored previous mutations + // Remove the idle state this.isIdle = false this._tryAddCustomEvent('sessionNoLongerIdle', { reason: 'user activity', @@ -752,10 +751,19 @@ export class SessionRecording { if (this._fullSnapshotTimer) { clearInterval(this._fullSnapshotTimer) } + // we don't schedule snapshots while idle + if (this.isIdle) { + return + } + + const interval = this.fullSnapshotIntervalMillis + if (!interval) { + return + } this._fullSnapshotTimer = setInterval(() => { this._tryTakeFullSnapshot() - }, FIVE_MINUTES) // 5 minutes + }, interval) } private _gatherRRWebPlugins() { @@ -800,8 +808,8 @@ export class SessionRecording { this._pageViewFallBack() } + // we're processing a full snapshot, so we should reset the timer if (rawEvent.type === EventType.FullSnapshot) { - // we're processing a full snapshot, so we should reset the timer this._scheduleFullSnapshot() } @@ -864,20 +872,8 @@ export class SessionRecording { const itemsToProcess = [...this.queuedRRWebEvents] this.queuedRRWebEvents = [] itemsToProcess.forEach((queuedRRWebEvent) => { - if (Date.now() - queuedRRWebEvent.enqueuedAt > TWO_SECONDS) { - this._tryAddCustomEvent('rrwebQueueTimeout', { - enqueuedAt: queuedRRWebEvent.enqueuedAt, - attempt: queuedRRWebEvent.attempt, - queueLength: itemsToProcess.length, - }) - } else { - if (this._tryRRWebMethod(queuedRRWebEvent)) { - this._tryAddCustomEvent('rrwebQueueSuccess', { - enqueuedAt: queuedRRWebEvent.enqueuedAt, - attempt: queuedRRWebEvent.attempt, - queueLength: itemsToProcess.length, - }) - } + if (Date.now() - queuedRRWebEvent.enqueuedAt <= TWO_SECONDS) { + this._tryRRWebMethod(queuedRRWebEvent) } }) } diff --git a/src/types.ts b/src/types.ts index 4be1ae3aa..03fa59ea6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -223,6 +223,8 @@ export interface SessionRecordingOptions { // our settings here only support a subset of those proposed for rrweb's network capture plugin recordHeaders?: boolean recordBody?: boolean + // ADVANCED: while a user is active we take a full snapshot of the browser every interval. For very few sites playback performance might be better with different interval. Set to 0 to disable + full_snapshot_interval_millis?: number } export type SessionIdChangedCallback = (sessionId: string, windowId: string | null | undefined) => void From fc32d873eac7239f1db69577edcb4663d267984c Mon Sep 17 00:00:00 2001 From: pauldambra Date: Thu, 20 Jun 2024 10:32:44 +0000 Subject: [PATCH 03/17] chore: Bump version to 1.139.4 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1ca5e138..65cead996 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.139.4 - 2024-06-20 + +- fix: no scheduled snapshots while idle (#1254) +- feat: Allow bootstrapping session id (#1251) + ## 1.139.3 - 2024-06-18 - feat(surveys): add branching logic (#1247) diff --git a/package.json b/package.json index 8e424dd0b..72e8399ec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.3", + "version": "1.139.4", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 34523219ff19e3025b1245c241511c58091bab7f Mon Sep 17 00:00:00 2001 From: Ben White Date: Thu, 20 Jun 2024 14:50:50 +0200 Subject: [PATCH 04/17] fix: Allow no capture of $opt_in event (#1250) --- src/__tests__/consent.test.ts | 37 +++++++++++++++++++++++++++++++++++ src/posthog-core.ts | 10 +++++++--- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/__tests__/consent.test.ts b/src/__tests__/consent.test.ts index 27294d905..9a3c5861a 100644 --- a/src/__tests__/consent.test.ts +++ b/src/__tests__/consent.test.ts @@ -78,6 +78,43 @@ describe('consentManager', () => { expect(posthog.persistence?.disabled).toBe(true) }) + describe('opt out event', () => { + let onCapture = jest.fn() + beforeEach(() => { + onCapture = jest.fn() + posthog = createPostHog({ opt_out_capturing_by_default: true, _onCapture: onCapture }) + }) + + it('should send opt in event if not disabled', () => { + posthog.opt_in_capturing() + expect(onCapture).toHaveBeenCalledWith('$opt_in', expect.objectContaining({})) + }) + + it('should send opt in event with overrides', () => { + posthog.opt_in_capturing({ + captureEventName: 'override-opt-in', + captureProperties: { + foo: 'bar', + }, + }) + expect(onCapture).toHaveBeenCalledWith( + 'override-opt-in', + expect.objectContaining({ + properties: expect.objectContaining({ + foo: 'bar', + }), + }) + ) + }) + + it('should not send opt in event if null or false', () => { + posthog.opt_in_capturing({ captureEventName: null }) + expect(onCapture).not.toHaveBeenCalled() + posthog.opt_in_capturing({ captureEventName: false }) + expect(onCapture).not.toHaveBeenCalled() + }) + }) + describe('with do not track setting', () => { beforeEach(() => { ;(navigator as any).doNotTrack = '1' diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 882630934..1c2921b97 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -1920,17 +1920,21 @@ export class PostHog { * }); * * @param {Object} [config] A dictionary of config options to override - * @param {string} [config.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action + * @param {string} [config.capture_event_name=$opt_in] Event name to be used for capturing the opt-in action. Set to `null` or `false` to skip capturing the optin event * @param {Object} [config.capture_properties] Set of properties to be captured along with the opt-in action */ opt_in_capturing(options?: { - captureEventName?: string /** event name to be used for capturing the opt-in action */ + captureEventName?: string | null | false /** event name to be used for capturing the opt-in action */ captureProperties?: Properties /** set of properties to be captured along with the opt-in action */ }): void { this.consent.optInOut(true) this._sync_opt_out_with_persistence() - // TODO: Do we need it to be sent instantly? + if (!isUndefined(options?.captureEventName) && !options?.captureEventName) { + // Don't capture if captureEventName is null or false + return + } + this.capture(options?.captureEventName ?? '$opt_in', options?.captureProperties, { send_instantly: true }) } From 8523d136f32ac4b85dd399285fa1587cbcbd6fe3 Mon Sep 17 00:00:00 2001 From: benjackwhite Date: Thu, 20 Jun 2024 12:51:26 +0000 Subject: [PATCH 05/17] chore: Bump version to 1.139.5 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cead996..8121ec410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.139.5 - 2024-06-20 + +- fix: Allow no capture of $opt_in event (#1250) + ## 1.139.4 - 2024-06-20 - fix: no scheduled snapshots while idle (#1254) diff --git a/package.json b/package.json index 72e8399ec..a34e215f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.4", + "version": "1.139.5", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From a43660a963c1221063a786d49a4b83c6a293f283 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Thu, 20 Jun 2024 20:09:52 +0200 Subject: [PATCH 06/17] fix(surveys): handle missing getNextSurveyStep (#1260) --- src/extensions/surveys.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/extensions/surveys.tsx b/src/extensions/surveys.tsx index c2dd1c6d3..33af0b58d 100644 --- a/src/extensions/surveys.tsx +++ b/src/extensions/surveys.tsx @@ -372,7 +372,9 @@ export function Questions({ setQuestionsResponses({ ...questionsResponses, [responseKey]: res }) - const nextStep = posthog.getNextSurveyStep(survey, displayQuestionIndex, res) + const nextStep = posthog.getNextSurveyStep + ? posthog.getNextSurveyStep(survey, displayQuestionIndex, res) + : displayQuestionIndex + 1 if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) } else { From 597ea6fa9772595f766a85a891b26f52bc452cde Mon Sep 17 00:00:00 2001 From: jurajmajerik Date: Thu, 20 Jun 2024 18:10:28 +0000 Subject: [PATCH 07/17] chore: Bump version to 1.139.6 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8121ec410..92b580e33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.139.6 - 2024-06-20 + +- fix(surveys): handle missing getNextSurveyStep (#1260) + ## 1.139.5 - 2024-06-20 - fix: Allow no capture of $opt_in event (#1250) diff --git a/package.json b/package.json index a34e215f8..d867dd71e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.5", + "version": "1.139.6", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From a1aad2a4a7d69f896b33f6e6c0733e26d3a55958 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 21 Jun 2024 08:00:45 +0200 Subject: [PATCH 08/17] fix(surveys): fix missing confirmation message state (#1263) --- src/extensions/surveys.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/extensions/surveys.tsx b/src/extensions/surveys.tsx index 33af0b58d..72ef641aa 100644 --- a/src/extensions/surveys.tsx +++ b/src/extensions/surveys.tsx @@ -372,9 +372,18 @@ export function Questions({ setQuestionsResponses({ ...questionsResponses, [responseKey]: res }) - const nextStep = posthog.getNextSurveyStep - ? posthog.getNextSurveyStep(survey, displayQuestionIndex, res) - : displayQuestionIndex + 1 + // Old SDK, no branching + if (!posthog.getNextSurveyStep) { + const isLastDisplayedQuestion = displayQuestionIndex === survey.questions.length - 1 + if (isLastDisplayedQuestion) { + sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) + } else { + setCurrentQuestionIndex(displayQuestionIndex + 1) + } + return + } + + const nextStep = posthog.getNextSurveyStep(survey, displayQuestionIndex, res) if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) } else { From 043706c77b321082fd16ff8e29e042b29aad2c8f Mon Sep 17 00:00:00 2001 From: jurajmajerik Date: Fri, 21 Jun 2024 06:01:26 +0000 Subject: [PATCH 09/17] chore: Bump version to 1.139.7 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92b580e33..4a326d3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.139.7 - 2024-06-21 + +- fix(surveys): fix missing confirmation message state (#1263) + ## 1.139.6 - 2024-06-20 - fix(surveys): handle missing getNextSurveyStep (#1260) diff --git a/package.json b/package.json index d867dd71e..4174ba0a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.6", + "version": "1.139.7", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 32fe902051e2d412abb91170af936d9181e56a96 Mon Sep 17 00:00:00 2001 From: Juraj Majerik Date: Fri, 21 Jun 2024 09:47:26 +0200 Subject: [PATCH 10/17] fix(surveys branching): rename confirmation_message -> end (#1257) --- src/__tests__/surveys.test.ts | 32 ++++++++++++++------------------ src/extensions/surveys.tsx | 2 +- src/posthog-core.ts | 4 ++-- src/posthog-surveys-types.ts | 12 ++++-------- src/posthog-surveys.ts | 14 +++++++------- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index ce2910e2c..5c67e01b3 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -801,29 +801,25 @@ describe('surveys', () => { } as Survey // Simple branching - it('when no branching specified, should return the index of the next question or confirmation_message', () => { + it('when no branching specified, should return the index of the next question or `end`', () => { survey.questions = [ { type: SurveyQuestionType.Open, question: 'Question A' }, { type: SurveyQuestionType.Open, question: 'Question B' }, ] as SurveyQuestion[] expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 1, 'Some response')).toEqual( - SurveyQuestionBranchingType.ConfirmationMessage - ) + expect(surveys.getNextSurveyStep(survey, 1, 'Some response')).toEqual(SurveyQuestionBranchingType.End) }) - it('should branch out to confirmation_message', () => { + it('should branch out to `end`', () => { survey.questions = [ { type: SurveyQuestionType.Open, question: 'Question A', - branching: { type: SurveyQuestionBranchingType.ConfirmationMessage }, + branching: { type: SurveyQuestionBranchingType.End }, }, { type: SurveyQuestionType.Open, question: 'Question B' }, ] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual( - SurveyQuestionBranchingType.ConfirmationMessage - ) + expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual(SurveyQuestionBranchingType.End) }) it('should branch out to a specific question', () => { @@ -937,7 +933,7 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'B', - branching: { type: SurveyQuestionBranchingType.ConfirmationMessage }, + branching: { type: SurveyQuestionBranchingType.End }, }, { type: SurveyQuestionType.Open, @@ -982,7 +978,7 @@ describe('surveys', () => { } expect(desiredOrder).toEqual(actualOrder) - expect(currentStep).toEqual(SurveyQuestionBranchingType.ConfirmationMessage) + expect(currentStep).toEqual(SurveyQuestionBranchingType.End) }) it('should display questions in the correct order in a multi-step NPS survey', () => { @@ -999,12 +995,12 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Sorry to hear that. Please enter your email, a colleague will be in touch.', - branching: { type: SurveyQuestionBranchingType.ConfirmationMessage }, + branching: { type: SurveyQuestionBranchingType.End }, }, { type: SurveyQuestionType.Open, question: 'Seems you are not completely happy. Tell us more!', - branching: { type: SurveyQuestionBranchingType.ConfirmationMessage }, + branching: { type: SurveyQuestionBranchingType.End }, }, { type: SurveyQuestionType.SingleChoice, @@ -1018,7 +1014,7 @@ describe('surveys', () => { { type: SurveyQuestionType.Link, question: 'Great! Here is the link:', - branching: { type: SurveyQuestionBranchingType.ConfirmationMessage }, + branching: { type: SurveyQuestionBranchingType.End }, }, { type: SurveyQuestionType.Open, @@ -1040,7 +1036,7 @@ describe('surveys', () => { currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) - expect(currentStep).toEqual(SurveyQuestionBranchingType.ConfirmationMessage) + expect(currentStep).toEqual(SurveyQuestionBranchingType.End) // Passive customer desiredOrder = ['How happy are you?', 'Seems you are not completely happy. Tell us more!'] @@ -1053,7 +1049,7 @@ describe('surveys', () => { currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) - expect(currentStep).toEqual(SurveyQuestionBranchingType.ConfirmationMessage) + expect(currentStep).toEqual(SurveyQuestionBranchingType.End) // Promoter customer, won't leave a review desiredOrder = ['How happy are you?', 'Glad to hear that! Will you leave us a review?', 'Curious, why not?'] @@ -1066,7 +1062,7 @@ describe('surveys', () => { currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) - expect(currentStep).toEqual(SurveyQuestionBranchingType.ConfirmationMessage) + expect(currentStep).toEqual(SurveyQuestionBranchingType.End) // Promoter customer, will leave a review desiredOrder = [ @@ -1083,7 +1079,7 @@ describe('surveys', () => { currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) - expect(currentStep).toEqual(SurveyQuestionBranchingType.ConfirmationMessage) + expect(currentStep).toEqual(SurveyQuestionBranchingType.End) }) it('should throw an error for an invalid scale', () => { diff --git a/src/extensions/surveys.tsx b/src/extensions/surveys.tsx index 72ef641aa..3bae8e71c 100644 --- a/src/extensions/surveys.tsx +++ b/src/extensions/surveys.tsx @@ -384,7 +384,7 @@ export function Questions({ } const nextStep = posthog.getNextSurveyStep(survey, displayQuestionIndex, res) - if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { + if (nextStep === SurveyQuestionBranchingType.End) { sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) } else { setCurrentQuestionIndex(nextStep) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 1c2921b97..21e61c1f1 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -1179,12 +1179,12 @@ export class PostHog { this.surveys.getActiveMatchingSurveys(callback, forceReload) } - /** Get the next step of the survey: a question index or confirmation_message */ + /** Get the next step of the survey: a question index or `end` */ getNextSurveyStep( survey: Survey, currentQuestionIndex: number, response: string | string[] | number | null - ): number | SurveyQuestionBranchingType.ConfirmationMessage { + ): number | SurveyQuestionBranchingType.End { return this.surveys.getNextSurveyStep(survey, currentQuestionIndex, response) } diff --git a/src/posthog-surveys-types.ts b/src/posthog-surveys-types.ts index bcaec38ac..204d78151 100644 --- a/src/posthog-surveys-types.ts +++ b/src/posthog-surveys-types.ts @@ -53,11 +53,7 @@ interface SurveyQuestionBase { optional?: boolean buttonText?: string originalQuestionIndex: number - branching?: - | NextQuestionBranching - | ConfirmationMessageBranching - | ResponseBasedBranching - | SpecificQuestionBranching + branching?: NextQuestionBranching | EndBranching | ResponseBasedBranching | SpecificQuestionBranching } export interface BasicSurveyQuestion extends SurveyQuestionBase { @@ -94,7 +90,7 @@ export enum SurveyQuestionType { export enum SurveyQuestionBranchingType { NextQuestion = 'next_question', - ConfirmationMessage = 'confirmation_message', + End = 'end', ResponseBased = 'response_based', SpecificQuestion = 'specific_question', } @@ -103,8 +99,8 @@ interface NextQuestionBranching { type: SurveyQuestionBranchingType.NextQuestion } -interface ConfirmationMessageBranching { - type: SurveyQuestionBranchingType.ConfirmationMessage +interface EndBranching { + type: SurveyQuestionBranchingType.End } interface ResponseBasedBranching { diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index dbe4ee6da..8bc89387e 100644 --- a/src/posthog-surveys.ts +++ b/src/posthog-surveys.ts @@ -192,14 +192,14 @@ export class PostHogSurveys { if (!question.branching?.type) { if (currentQuestionIndex === survey.questions.length - 1) { - return SurveyQuestionBranchingType.ConfirmationMessage + return SurveyQuestionBranchingType.End } return nextQuestionIndex } - if (question.branching.type === SurveyQuestionBranchingType.ConfirmationMessage) { - return SurveyQuestionBranchingType.ConfirmationMessage + if (question.branching.type === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End } else if (question.branching.type === SurveyQuestionBranchingType.SpecificQuestion) { if (Number.isInteger(question.branching.index)) { return question.branching.index @@ -219,8 +219,8 @@ export class PostHogSurveys { return nextStep } - if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { - return SurveyQuestionBranchingType.ConfirmationMessage + if (nextStep === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End } return nextQuestionIndex @@ -240,8 +240,8 @@ export class PostHogSurveys { return nextStep } - if (nextStep === SurveyQuestionBranchingType.ConfirmationMessage) { - return SurveyQuestionBranchingType.ConfirmationMessage + if (nextStep === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End } return nextQuestionIndex From fd5ff2c786403940e27013d2945e642548ed4671 Mon Sep 17 00:00:00 2001 From: jurajmajerik Date: Fri, 21 Jun 2024 07:48:01 +0000 Subject: [PATCH 11/17] chore: Bump version to 1.139.8 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a326d3ff..4574a1ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.139.8 - 2024-06-21 + +- fix(surveys branching): rename confirmation_message -> end (#1257) + ## 1.139.7 - 2024-06-21 - fix(surveys): fix missing confirmation message state (#1263) diff --git a/package.json b/package.json index 4174ba0a7..8e7abdf8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.7", + "version": "1.139.8", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 004351a7c51908fe43ddc9830603beb5681291cc Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 21 Jun 2024 14:54:40 +0100 Subject: [PATCH 12/17] chore: add sanitization test to heatmaps tests (#1256) --- src/__tests__/heatmaps.test.ts | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/__tests__/heatmaps.test.ts b/src/__tests__/heatmaps.test.ts index 73eb1fa6a..8313fa5c7 100644 --- a/src/__tests__/heatmaps.test.ts +++ b/src/__tests__/heatmaps.test.ts @@ -2,6 +2,8 @@ import { createPosthogInstance } from './helpers/posthog-instance' import { uuidv7 } from '../uuidv7' import { PostHog } from '../posthog-core' import { DecideResponse } from '../types' +import { isObject } from '../utils/type-utils' + jest.mock('../utils/logger') describe('heatmaps', () => { @@ -18,7 +20,25 @@ describe('heatmaps', () => { beforeEach(async () => { onCapture = jest.fn() - posthog = await createPosthogInstance(uuidv7(), { _onCapture: onCapture }) + posthog = await createPosthogInstance(uuidv7(), { + _onCapture: onCapture, + sanitize_properties: (props) => { + // what ever sanitization makes sense + const sanitizeUrl = (url: string) => url.replace(/https?:\/\/[^/]+/g, 'http://replaced') + if (props['$current_url']) { + props['$current_url'] = sanitizeUrl(props['$current_url']) + } + if (isObject(props['$heatmap_data'])) { + // the keys of the heatmap data are URLs, so we might need to sanitize them to + // this sanitized URL would need to be entered in the toolbar for the heatmap display to work + props['$heatmap_data'] = Object.entries(props['$heatmap_data']).reduce((acc, [url, data]) => { + acc[sanitizeUrl(url)] = data + return acc + }, {}) + } + return props + }, + }) }) it('should include generated heatmap data', async () => { @@ -32,7 +52,7 @@ describe('heatmaps', () => { event: 'test event', properties: { $heatmap_data: { - 'http://localhost/': [ + 'http://replaced/': [ { target_fixed: false, type: 'click', @@ -54,8 +74,8 @@ describe('heatmaps', () => { posthog.capture('test event') expect(onCapture).toBeCalledTimes(1) - expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/']).toHaveLength(4) - expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/'].map((x) => x.type)).toEqual([ + expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://replaced/']).toHaveLength(4) + expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://replaced/'].map((x) => x.type)).toEqual([ 'click', 'click', 'rageclick', @@ -68,7 +88,7 @@ describe('heatmaps', () => { posthog.heatmaps?.['_onClick']?.(createMockMouseEvent()) posthog.capture('test event') expect(onCapture).toBeCalledTimes(1) - expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://localhost/']).toHaveLength(2) + expect(onCapture.mock.lastCall[1].properties.$heatmap_data['http://replaced/']).toHaveLength(2) posthog.capture('test event 2') expect(onCapture).toBeCalledTimes(2) From 62c51810bbc4e819376aad350700ee390ee29c12 Mon Sep 17 00:00:00 2001 From: Robbie Date: Fri, 21 Jun 2024 14:56:00 +0100 Subject: [PATCH 13/17] feat(web-analytics): Add property for autocapture link click (#1259) * Add property for autocapture link click * Only capture external urls * Add tests * Add a test * Rename property * Update src/__tests__/autocapture.test.ts Co-authored-by: Marius Andra --------- Co-authored-by: Marius Andra --- playground/nextjs/pages/index.tsx | 1 + src/__tests__/autocapture.test.ts | 23 ++++++++++++++++++++++- src/autocapture.ts | 8 ++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/playground/nextjs/pages/index.tsx b/playground/nextjs/pages/index.tsx index 5aad9d388..a7ce544c0 100644 --- a/playground/nextjs/pages/index.tsx +++ b/playground/nextjs/pages/index.tsx @@ -36,6 +36,7 @@ export default function Home() { Autocapture a > span + External link diff --git a/src/__tests__/autocapture.test.ts b/src/__tests__/autocapture.test.ts index acdc9b147..c79edf87d 100644 --- a/src/__tests__/autocapture.test.ts +++ b/src/__tests__/autocapture.test.ts @@ -557,6 +557,7 @@ describe('Autocapture system', () => { expect(props['$elements'][1]).toHaveProperty('tag_name', 'span') expect(props['$elements'][2]).toHaveProperty('tag_name', 'div') expect(props['$elements'][props['$elements'].length - 1]).toHaveProperty('tag_name', 'body') + expect(props['$external_click_url']).toEqual('https://test.com') }) it('truncate any element property value to 1024 bytes', () => { @@ -594,7 +595,27 @@ describe('Autocapture system', () => { target: elTarget, }) ) - expect(captureMock.mock.calls[0][1]['$elements'][0]).toHaveProperty('attr__href', 'https://test.com') + const props = captureMock.mock.calls[0][1] + expect(props['$elements'][0]).toHaveProperty('attr__href', 'https://test.com') + expect(props['$external_click_url']).toEqual('https://test.com') + }) + + it('does not include $click_external_href for same site', () => { + window!.location = new URL('https://www.example.com/location') as unknown as Location + const elTarget = document.createElement('img') + const elParent = document.createElement('span') + elParent.appendChild(elTarget) + const elGrandparent = document.createElement('a') + elGrandparent.setAttribute('href', 'https://www.example.com/link') + elGrandparent.appendChild(elParent) + autocapture['_captureEvent']( + makeMouseEvent({ + target: elTarget, + }) + ) + const props = captureMock.mock.calls[0][1] + expect(props['$elements'][0]).toHaveProperty('attr__href', 'https://www.example.com/link') + expect(props['$external_click_url']).toBeUndefined() }) it('does not capture href attribute values from password elements', () => { diff --git a/src/autocapture.ts b/src/autocapture.ts index 5b9baffcc..cb36bbeb9 100644 --- a/src/autocapture.ts +++ b/src/autocapture.ts @@ -25,6 +25,7 @@ import { AUTOCAPTURE_DISABLED_SERVER_SIDE } from './constants' import { isBoolean, isFunction, isNull, isObject, isUndefined } from './utils/type-utils' import { logger } from './utils/logger' import { document, window } from './utils/globals' +import { convertToURL } from './utils/request-utils' const COPY_AUTOCAPTURE_EVENT = '$copy_autocapture' @@ -322,8 +323,14 @@ export class Autocapture { } } + let externalHref: string | undefined if (href) { elementsJson[0]['attr__href'] = href + const hrefHost = convertToURL(href)?.host + const locationHost = window?.location?.host + if (hrefHost && locationHost && hrefHost !== locationHost) { + externalHref = href + } } if (explicitNoCapture) { @@ -340,6 +347,7 @@ export class Autocapture { $elements: elementsJson, }, elementsJson[0]?.['$el_text'] ? { $el_text: elementsJson[0]?.['$el_text'] } : {}, + externalHref && e.type === 'click' ? { $external_click_url: externalHref } : {}, autocaptureAugmentProperties ) From 369484a8a97fa7c2c17a5f799b58262dabd79938 Mon Sep 17 00:00:00 2001 From: robbie-c Date: Fri, 21 Jun 2024 13:56:33 +0000 Subject: [PATCH 14/17] chore: Bump version to 1.140.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4574a1ed3..c8eb87508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.140.0 - 2024-06-21 + +- feat(web-analytics): Add property for autocapture link click (#1259) +- chore: add sanitization test to heatmaps tests (#1256) + ## 1.139.8 - 2024-06-21 - fix(surveys branching): rename confirmation_message -> end (#1257) diff --git a/package.json b/package.json index 8e7abdf8c..47c016acc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.139.8", + "version": "1.140.0", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 4b5cce1f5b40b81dedee5401fd5fb6ad54922fac Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Fri, 21 Jun 2024 18:22:06 +0100 Subject: [PATCH 15/17] fix: unwind recent changes things seem less stable (#1267) Co-authored-by: David Newell --- .../replay/sessionrecording.test.ts | 37 ++------ src/extensions/replay/sessionrecording.ts | 86 ++++++------------- 2 files changed, 33 insertions(+), 90 deletions(-) diff --git a/src/__tests__/extensions/replay/sessionrecording.test.ts b/src/__tests__/extensions/replay/sessionrecording.test.ts index ab895de2a..dde4aae1f 100644 --- a/src/__tests__/extensions/replay/sessionrecording.test.ts +++ b/src/__tests__/extensions/replay/sessionrecording.test.ts @@ -1323,7 +1323,7 @@ describe('SessionRecording', () => { }) }) - it('emits full snapshot events even when idle', () => { + it('drops full snapshots when idle - so we must make sure not to take them while idle!', () => { // force idle state sessionRecording['isIdle'] = true // buffer is empty @@ -1336,19 +1336,14 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createFullSnapshot({}) as eventWithTime) expect(sessionRecording['buffer']).toEqual({ - data: [ - { - data: {}, - type: 2, - }, - ], + data: [], sessionId: sessionId, - size: 20, + size: 0, windowId: 'windowId', }) }) - it('emits meta snapshot events even when idle', () => { + it('does not emit meta snapshot events when idle - so we must make sure not to take them while idle!', () => { // force idle state sessionRecording['isIdle'] = true // buffer is empty @@ -1361,21 +1356,14 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createMetaSnapshot({}) as eventWithTime) expect(sessionRecording['buffer']).toEqual({ - data: [ - { - data: { - href: 'https://has-to-be-present-or-invalid.com', - }, - type: 4, - }, - ], + data: [], sessionId: sessionId, - size: 69, + size: 0, windowId: 'windowId', }) }) - it('emits style snapshot events even when idle', () => { + it('does not emit style snapshot events when idle - so we must make sure not to take them while idle!', () => { // force idle state sessionRecording['isIdle'] = true // buffer is empty @@ -1388,16 +1376,9 @@ describe('SessionRecording', () => { sessionRecording.onRRwebEmit(createStyleSnapshot({}) as eventWithTime) expect(sessionRecording['buffer']).toEqual({ - data: [ - { - data: { - source: 13, - }, - type: 3, - }, - ], + data: [], sessionId: sessionId, - size: 31, + size: 0, windowId: 'windowId', }) }) diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts index d138cf921..61e3c796e 100644 --- a/src/extensions/replay/sessionrecording.ts +++ b/src/extensions/replay/sessionrecording.ts @@ -61,27 +61,6 @@ const ACTIVE_SOURCES = [ IncrementalSource.Drag, ] -const STYLE_SOURCES = [ - IncrementalSource.StyleSheetRule, - IncrementalSource.StyleDeclaration, - IncrementalSource.AdoptedStyleSheet, - IncrementalSource.Font, -] - -const TYPES_ALLOWED_WHEN_IDLE = [EventType.Custom, EventType.Meta, EventType.FullSnapshot] - -/** - * we want to restrict the data allowed when we've detected an idle session - * but allow data that the player might require for proper playback - */ -function allowedWhenIdle(event: eventWithTime): boolean { - const isAllowedIncremental = - event.type === EventType.IncrementalSnapshot && - !isNullish(event.data.source) && - STYLE_SOURCES.includes(event.data.source) - return TYPES_ALLOWED_WHEN_IDLE.includes(event.type) || isAllowedIncremental -} - /** * Session recording starts in buffering mode while waiting for decide response * Once the response is received it might be disabled, active or sampled @@ -95,33 +74,6 @@ interface SnapshotBuffer { data: any[] sessionId: string windowId: string - - readonly mostRecentSnapshotTimestamp: number | null - - add(properties: Properties): void -} - -class InMemoryBuffer implements SnapshotBuffer { - size: number - data: any[] - sessionId: string - windowId: string - - get mostRecentSnapshotTimestamp(): number | null { - return this.data.length ? this.data[this.data.length - 1].timestamp : null - } - - constructor(sessionId: string, windowId: string) { - this.size = 0 - this.data = [] - this.sessionId = sessionId - this.windowId = windowId - } - - add(properties: Properties) { - this.size += properties.$snapshot_bytes - this.data.push(properties.$snapshot_data) - } } interface QueuedRRWebEvent { @@ -206,7 +158,7 @@ export class SessionRecording { private get sessionManager() { if (!this.instance.sessionManager) { - throw new Error(LOGGER_PREFIX + ' started without valid sessionManager. This is a bug.') + throw new Error(LOGGER_PREFIX + ' must be started with a valid sessionManager.') } return this.instance.sessionManager @@ -222,9 +174,9 @@ export class SessionRecording { } private get sessionDuration(): number | null { - const mostRecentSnapshotTimestamp = this.buffer.mostRecentSnapshotTimestamp + const mostRecentSnapshot = this.buffer?.data[this.buffer?.data.length - 1] const { sessionStartTimestamp } = this.sessionManager.checkAndGetSessionAndWindowId(true) - return mostRecentSnapshotTimestamp ? mostRecentSnapshotTimestamp - sessionStartTimestamp : null + return mostRecentSnapshot ? mostRecentSnapshot.timestamp - sessionStartTimestamp : null } private get isRecordingEnabled() { @@ -341,7 +293,7 @@ export class SessionRecording { this.sessionId = sessionId this.windowId = windowId - this.buffer = new InMemoryBuffer(this.sessionId, this.windowId) + this.buffer = this.clearBuffer() // on reload there might be an already sampled session that should be continued before decide response, // so we call this here _and_ in the decide response @@ -827,8 +779,9 @@ export class SessionRecording { this._updateWindowAndSessionIds(event) - if (this.isIdle && !allowedWhenIdle(event)) { - // When in an idle state we keep recording, but don't capture all events + // When in an idle state we keep recording, but don't capture the events + // but we allow custom events even when idle + if (this.isIdle && event.type !== EventType.Custom) { return } @@ -897,11 +850,17 @@ export class SessionRecording { return url } - private clearBuffer(): void { - this.buffer = new InMemoryBuffer(this.sessionId, this.windowId) + private clearBuffer(): SnapshotBuffer { + this.buffer = { + size: 0, + data: [], + sessionId: this.sessionId, + windowId: this.windowId, + } + return this.buffer } - private _flushBuffer(): void { + private _flushBuffer(): SnapshotBuffer { if (this.flushBufferTimer) { clearTimeout(this.flushBufferTimer) this.flushBufferTimer = undefined @@ -919,8 +878,7 @@ export class SessionRecording { this.flushBufferTimer = setTimeout(() => { this._flushBuffer() }, RECORDING_BUFFER_TIMEOUT) - - return + return this.buffer } if (this.buffer.data.length > 0) { @@ -931,7 +889,9 @@ export class SessionRecording { $window_id: this.buffer.windowId, }) } - this.clearBuffer() + + // buffer is empty, we clear it in case the session id has changed + return this.clearBuffer() } private _captureSnapshotBuffered(properties: Properties) { @@ -940,10 +900,12 @@ export class SessionRecording { this.buffer.size + properties.$snapshot_bytes + additionalBytes > RECORDING_MAX_EVENT_SIZE || this.buffer.sessionId !== this.sessionId ) { - this._flushBuffer() + this.buffer = this._flushBuffer() } - this.buffer.add(properties) + this.buffer.size += properties.$snapshot_bytes + this.buffer.data.push(properties.$snapshot_data) + if (!this.flushBufferTimer) { this.flushBufferTimer = setTimeout(() => { this._flushBuffer() From 70d167fe67365f30b2ec8a720f31a111a4dfae1b Mon Sep 17 00:00:00 2001 From: pauldambra Date: Fri, 21 Jun 2024 17:22:42 +0000 Subject: [PATCH 16/17] chore: Bump version to 1.140.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8eb87508..845c3422a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.140.1 - 2024-06-21 + +- fix: unwind recent changes things seem less stable (#1267) + ## 1.140.0 - 2024-06-21 - feat(web-analytics): Add property for autocapture link click (#1259) diff --git a/package.json b/package.json index 47c016acc..69ff2e6d9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "posthog-js", - "version": "1.140.0", + "version": "1.140.1", "description": "Posthog-js allows you to automatically capture usage and send events to PostHog.", "repository": "https://github.com/PostHog/posthog-js", "author": "hey@posthog.com", From 3017d39afb80da6b4de895c2d974128b9bde931e Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Sat, 22 Jun 2024 09:23:32 +0100 Subject: [PATCH 17/17] chore: update pnpm action setup worfklow version (#1268) --- .github/workflows/bundled-size.yaml | 2 +- .github/workflows/cd.yml | 4 ++-- .github/workflows/label-version-bump.yml | 2 +- .github/workflows/library-ci.yml | 8 ++++---- .github/workflows/react.yml | 2 +- .github/workflows/ssr-es-check.yml | 2 +- .github/workflows/testcafe.yml | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/bundled-size.yaml b/.github/workflows/bundled-size.yaml index a468b576f..87531a7ea 100644 --- a/.github/workflows/bundled-size.yaml +++ b/.github/workflows/bundled-size.yaml @@ -10,7 +10,7 @@ jobs: - uses: actions/checkout@v2 - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 with: version: 8.x.x diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 580832dd6..749ecf2d2 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -36,7 +36,7 @@ jobs: fetch-depth: 0 token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x @@ -109,7 +109,7 @@ jobs: token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 - name: Set up Node.js uses: actions/setup-node@v3 diff --git a/.github/workflows/label-version-bump.yml b/.github/workflows/label-version-bump.yml index e57d312d5..e0177036b 100644 --- a/.github/workflows/label-version-bump.yml +++ b/.github/workflows/label-version-bump.yml @@ -23,7 +23,7 @@ jobs: token: ${{ secrets.POSTHOG_BOT_GITHUB_TOKEN }} fetch-depth: 0 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x diff --git a/.github/workflows/library-ci.yml b/.github/workflows/library-ci.yml index d9c76dbf3..ca6dbcc6e 100644 --- a/.github/workflows/library-ci.yml +++ b/.github/workflows/library-ci.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 @@ -27,7 +27,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 @@ -76,7 +76,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 diff --git a/.github/workflows/react.yml b/.github/workflows/react.yml index 37c3f89e6..37973e5e2 100644 --- a/.github/workflows/react.yml +++ b/.github/workflows/react.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 diff --git a/.github/workflows/ssr-es-check.yml b/.github/workflows/ssr-es-check.yml index 39dbd1ccc..0e9fb075e 100644 --- a/.github/workflows/ssr-es-check.yml +++ b/.github/workflows/ssr-es-check.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4 diff --git a/.github/workflows/testcafe.yml b/.github/workflows/testcafe.yml index 7ca96a4c1..a7cd6eea9 100644 --- a/.github/workflows/testcafe.yml +++ b/.github/workflows/testcafe.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 with: version: 8.x.x - uses: actions/setup-node@v4