Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle uninitialised helpers better #767

Merged
merged 7 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/__tests__/posthog-core.identify.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ jest.mock('../gdpr-utils', () => ({
}))
jest.mock('../decide')

given('lib', () => Object.assign(new PostHog(), given.overrides))
given('lib', () => {
const posthog = new PostHog()
posthog._init('testtoken', given.config, 'testhog')
return Object.assign(posthog, given.overrides)
})

describe('identify()', () => {
given(
Expand Down Expand Up @@ -339,10 +343,6 @@ describe('reset()', () => {
persistence: new PostHogPersistence(given.config),
}))

beforeEach(() => {
given.lib._init('testtoken', given.config, 'testhog')
})

it('clears persistence', () => {
given.lib.persistence.register({ $enabled_feature_flags: { flag: 'variant', other: true } })
expect(given.lib.persistence.props['$enabled_feature_flags']).toEqual({ flag: 'variant', other: true })
Expand Down
9 changes: 8 additions & 1 deletion src/__tests__/posthog-core.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ jest.mock('../utils', () => ({
document: { title: 'test' },
}))

given('lib', () => Object.assign(new PostHog(), given.overrides))
given('lib', () => {
const posthog = new PostHog()
posthog._init('testtoken', given.config, 'testhog')
return Object.assign(posthog, given.overrides)
})

describe('capture()', () => {
given('eventName', () => '$event')
Expand All @@ -29,6 +33,7 @@ describe('capture()', () => {
given('config', () => ({
property_blacklist: [],
_onCapture: jest.fn(),
get_device_id: jest.fn().mockReturnValue('device-id'),
}))

given('overrides', () => ({
Expand All @@ -38,11 +43,13 @@ describe('capture()', () => {
persistence: {
remove_event_timer: jest.fn(),
properties: jest.fn(),
update_config: jest.fn(),
},
sessionPersistence: {
update_search_keyword: jest.fn(),
update_campaign_params: jest.fn(),
update_referrer_info: jest.fn(),
update_config: jest.fn(),
properties: jest.fn(),
},
compression: {},
Expand Down
35 changes: 26 additions & 9 deletions src/extensions/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,15 @@ export class SessionRecording {
})
}

private getSessionManager() {
if (!this.instance.sessionManager) {
logger.error('Session recording started without valid sessionManager')
return
}

return this.instance.sessionManager
}

startRecordingIfEnabled() {
if (this.isRecordingEnabled()) {
this.startCaptureAndTrySendingQueuedSnapshots()
Expand Down Expand Up @@ -185,14 +194,18 @@ export class SessionRecording {
}

private _startCapture() {
// According to the rrweb docs, rrweb is not supported on IE11 and below:
// "rrweb does not support IE11 and below because it uses the MutationObserver API which was supported by these browsers."
// https://github.com/rrweb-io/rrweb/blob/master/guide.md#compatibility-note
//
// However, MutationObserver does exist on IE11, it just doesn't work well and does not detect all changes.
// Instead, when we load "recorder.js", the first JS error is about "Object.assign" being undefined.
// Thus instead of MutationObserver, we look for this function and block recording if it's undefined.
const sessionManager = this.getSessionManager()
if (!sessionManager) {
return
}
if (typeof Object.assign === 'undefined') {
// According to the rrweb docs, rrweb is not supported on IE11 and below:
// "rrweb does not support IE11 and below because it uses the MutationObserver API which was supported by these browsers."
// https://github.com/rrweb-io/rrweb/blob/master/guide.md#compatibility-note
//
// However, MutationObserver does exist on IE11, it just doesn't work well and does not detect all changes.
// Instead, when we load "recorder.js", the first JS error is about "Object.assign" being undefined.
// Thus instead of MutationObserver, we look for this function and block recording if it's undefined.
return
}

Expand All @@ -203,7 +216,7 @@ export class SessionRecording {

this.captureStarted = true
// We want to ensure the sessionManager is reset if necessary on load of the recorder
this.instance.sessionManager.checkAndGetSessionAndWindowId()
sessionManager.checkAndGetSessionAndWindowId()

const recorderJS = this.getRecordingVersion() === 'v2' ? 'recorder-v2.js' : 'recorder.js'

Expand Down Expand Up @@ -231,6 +244,10 @@ export class SessionRecording {
}

private _updateWindowAndSessionIds(event: eventWithTime) {
const sessionManager = this.getSessionManager()
if (!sessionManager) {
return
}
// Some recording events are triggered by non-user events (e.g. "X minutes ago" text updating on the screen).
// We don't want to extend the session or trigger a new session in these cases. These events are designated by event
// type -> incremental update, and source -> mutation.
Expand Down Expand Up @@ -258,7 +275,7 @@ export class SessionRecording {
}

// We only want to extend the session if it is an interactive event.
const { windowId, sessionId } = this.instance.sessionManager.checkAndGetSessionAndWindowId(
const { windowId, sessionId } = sessionManager.checkAndGetSessionAndWindowId(
!isUserInteraction,
event.timestamp
)
Expand Down
91 changes: 49 additions & 42 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,20 +260,21 @@ export class PostHog {
__loaded_recorder_version: 'v1' | 'v2' | undefined // flag that keeps track of which version of recorder is loaded
config: PostHogConfig

persistence: PostHogPersistence
rateLimiter: RateLimiter
sessionPersistence: PostHogPersistence
sessionManager: SessionIdManager
pageViewIdManager: PageViewIdManager
featureFlags: PostHogFeatureFlags
surveys: PostHogSurveys
toolbar: Toolbar
sessionRecording: SessionRecording | undefined
webPerformance: WebPerformanceObserver | undefined
exceptionAutocapture: ExceptionObserver | undefined

_requestQueue: RequestQueue
_retryQueue: RetryQueue
// These are instance-specific state created after initialisation
persistence?: PostHogPersistence
sessionPersistence?: PostHogPersistence
sessionManager?: SessionIdManager
_requestQueue?: RequestQueue
_retryQueue?: RetryQueue
sessionRecording?: SessionRecording
webPerformance?: WebPerformanceObserver
exceptionAutocapture?: ExceptionObserver

_triggered_notifs: any
compression: Partial<Record<Compression, boolean>>
Expand Down Expand Up @@ -314,13 +315,6 @@ export class PostHog {
this.surveys = new PostHogSurveys(this)
this.rateLimiter = new RateLimiter()

// these are created in _init() after we have the config
this._requestQueue = undefined as any
this._retryQueue = undefined as any
this.persistence = undefined as any
this.sessionPersistence = undefined as any
this.sessionManager = undefined as any

// NOTE: See the property definition for deprecation notice
this.people = {
set: (prop: string | Properties, to?: string, callback?: RequestCallback) => {
Expand Down Expand Up @@ -562,7 +556,7 @@ export class PostHog {
_start_queue_if_opted_in(): void {
if (!this.has_opted_out_capturing()) {
if (this.get_config('request_batching')) {
this._requestQueue.poll()
this._requestQueue?.poll()
}
}
}
Expand Down Expand Up @@ -625,8 +619,8 @@ export class PostHog {
this.capture('$pageleave')
}

this._requestQueue.unload()
this._retryQueue.unload()
this._requestQueue?.unload()
this._retryQueue?.unload()
}

_handle_queued_event(url: string, data: Record<string, any>, options?: XHROptions): void {
Expand All @@ -645,6 +639,9 @@ export class PostHog {
}

_send_request(url: string, data: Record<string, any>, options: CaptureOptions, callback?: RequestCallback): void {
if (!this.__loaded || !this._retryQueue) {
return
}
if (this.rateLimiter.isRateLimited(options._batchKey)) {
if (this.get_config('debug')) {
console.warn('[PostHog SendRequest] is quota limited. Dropping request.')
Expand Down Expand Up @@ -837,7 +834,7 @@ export class PostHog {
): CaptureResult | void {
// While developing, a developer might purposefully _not_ call init(),
// in this case, we would like capture to be a noop.
if (!this.__loaded) {
if (!this.__loaded || !this.sessionPersistence || !this._requestQueue) {
benjackwhite marked this conversation as resolved.
Show resolved Hide resolved
return
}

Expand Down Expand Up @@ -922,6 +919,10 @@ export class PostHog {
}

_calculate_event_properties(event_name: string, event_properties: Properties): Properties {
if (!this.persistence || !this.sessionPersistence) {
return event_properties
}

// set defaults
const start_timestamp = this.persistence.remove_event_timer(event_name)
let properties = { ...event_properties }
Expand Down Expand Up @@ -1019,7 +1020,7 @@ export class PostHog {
* @param {Number} [days] How many days since the user's last visit to store the super properties
*/
register(properties: Properties, days?: number): void {
this.persistence.register(properties, days)
this.persistence?.register(properties, days)
}

/**
Expand All @@ -1046,7 +1047,7 @@ export class PostHog {
* @param {Number} [days] How many days since the users last visit to store the super properties
*/
register_once(properties: Properties, default_value?: Property, days?: number): void {
this.persistence.register_once(properties, default_value, days)
this.persistence?.register_once(properties, default_value, days)
}

/**
Expand All @@ -1073,7 +1074,7 @@ export class PostHog {
* @param {Object} properties An associative array of properties to store about the user
*/
register_for_session(properties: Properties): void {
this.sessionPersistence.register(properties)
this.sessionPersistence?.register(properties)
}

/**
Expand All @@ -1082,7 +1083,7 @@ export class PostHog {
* @param {String} property The name of the super property to remove
*/
unregister(property: string): void {
this.persistence.unregister(property)
this.persistence?.unregister(property)
}

/**
Expand All @@ -1091,7 +1092,7 @@ export class PostHog {
* @param {String} property The name of the session super property to remove
*/
unregister_for_session(property: string): void {
this.sessionPersistence.unregister(property)
this.sessionPersistence?.unregister(property)
}

_register_single(prop: string, value: Property) {
Expand Down Expand Up @@ -1191,7 +1192,7 @@ export class PostHog {
* @returns {Function} A function that can be called to unsubscribe the listener. E.g. Used by useEffect when the component unmounts.
*/
onSessionId(callback: SessionIdChangedCallback): () => void {
return this.sessionManager.onSessionId(callback)
return this.sessionManager?.onSessionId(callback) ?? (() => {})
}

/** Get list of all surveys. */
Expand Down Expand Up @@ -1251,6 +1252,9 @@ export class PostHog {
* @param {Object} [userPropertiesToSetOnce] Optional: An associative array of properties to store about the user. If property is previously set, this does not override that value.
*/
identify(new_distinct_id?: string, userPropertiesToSet?: Properties, userPropertiesToSetOnce?: Properties): void {
if (!this.__loaded || !this.persistence) {
return
}
//if the new_distinct_id has not been set ignore the identify event
if (!new_distinct_id) {
console.error('Unique user id has not been set in posthog.identify')
Expand Down Expand Up @@ -1417,11 +1421,14 @@ export class PostHog {
* Useful for clearing data when a user logs out.
*/
reset(reset_device_id?: boolean): void {
if (!this.__loaded) {
return
}
const device_id = this.get_property('$device_id')
this.persistence.clear()
this.sessionPersistence.clear()
this.persistence.set_user_state('anonymous')
this.sessionManager.resetSessionId()
this.persistence?.clear()
this.sessionPersistence?.clear()
this.persistence?.set_user_state('anonymous')
this.sessionManager?.resetSessionId()
const uuid = this.get_config('get_device_id')(this.uuidFn())
this.register_once(
{
Expand Down Expand Up @@ -1464,7 +1471,7 @@ export class PostHog {
*/

get_session_id(): string {
return this.sessionManager.checkAndGetSessionAndWindowId(true).sessionId
return this.sessionManager?.checkAndGetSessionAndWindowId(true).sessionId ?? ''
benjackwhite marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand All @@ -1475,6 +1482,9 @@ export class PostHog {
* @param options.timestampLookBack How many seconds to look back for the timestamp (defaults to 10)
*/
get_session_replay_url(options?: { withTimestamp?: boolean; timestampLookBack?: number }): string {
if (!this.sessionManager) {
return ''
}
const host = this.config.ui_host || this.config.api_host
const { sessionId, sessionStartTimestamp } = this.sessionManager.checkAndGetSessionAndWindowId(true)
let url = host + '/replay/' + sessionId
Expand Down Expand Up @@ -1681,12 +1691,9 @@ export class PostHog {
this.config.disable_persistence = this.config.disable_cookie
}

if (this.persistence) {
this.persistence.update_config(this.config)
}
if (this.sessionPersistence) {
this.sessionPersistence.update_config(this.config)
}
this.persistence?.update_config(this.config)
this.sessionPersistence?.update_config(this.config)

if (localStore.is_supported() && localStore.get('ph_debug') === 'true') {
this.config.debug = true
}
Expand Down Expand Up @@ -1765,7 +1772,7 @@ export class PostHog {
* @param {String} property_name The name of the super property you want to retrieve
*/
get_property(property_name: string): Property | undefined {
return this.persistence['props'][property_name]
return this.persistence?.['props'][property_name]
}

/**
Expand All @@ -1788,7 +1795,7 @@ export class PostHog {
* @param {String} property_name The name of the session super property you want to retrieve
*/
getSessionProperty(property_name: string): Property | undefined {
return this.sessionPersistence['props'][property_name]
return this.sessionPersistence?.['props'][property_name]
}

toString(): string {
Expand Down Expand Up @@ -1851,11 +1858,11 @@ export class PostHog {
return
}

if (!this.get_config('disable_persistence') && this.persistence.disabled !== disabled) {
this.persistence.set_disabled(disabled)
if (!this.get_config('disable_persistence') && this.persistence?.disabled !== disabled) {
this.persistence?.set_disabled(disabled)
}
if (!this.get_config('disable_persistence') && this.sessionPersistence.disabled !== disabled) {
this.sessionPersistence.set_disabled(disabled)
if (!this.get_config('disable_persistence') && this.sessionPersistence?.disabled !== disabled) {
this.sessionPersistence?.set_disabled(disabled)
}
}

Expand Down
Loading