diff --git a/src/__tests__/extensions/toolbar.test.ts b/src/__tests__/extensions/toolbar.test.ts index c5cd49cfe..091b706f9 100644 --- a/src/__tests__/extensions/toolbar.test.ts +++ b/src/__tests__/extensions/toolbar.test.ts @@ -35,7 +35,6 @@ describe('Toolbar', () => { beforeEach(() => { assignableWindow.ph_load_toolbar = jest.fn() - delete assignableWindow['_postHogToolbarLoaded'] }) describe('maybeLoadToolbar', () => { @@ -176,6 +175,13 @@ describe('Toolbar', () => { expect(toolbar.loadToolbar(toolbarParams)).toBe(true) expect(toolbar.loadToolbar(toolbarParams)).toBe(false) }) + + it('should load if previously loaded but closed via localstorage', () => { + expect(toolbar.loadToolbar(toolbarParams)).toBe(true) + expect(toolbar.loadToolbar(toolbarParams)).toBe(false) + localStorage.removeItem('_postHogToolbarParams') + expect(toolbar.loadToolbar(toolbarParams)).toBe(true) + }) }) describe('load and close toolbar with minimal params', () => { diff --git a/src/extensions/toolbar.ts b/src/extensions/toolbar.ts index fc68a45e6..db15fabd1 100644 --- a/src/extensions/toolbar.ts +++ b/src/extensions/toolbar.ts @@ -11,8 +11,13 @@ const STATE_FROM_WINDOW = window?.location ? _getHashParam(window.location.hash, '__posthog') || _getHashParam(location.hash, 'state') : null +const LOCALSTORAGE_KEY = '_postHogToolbarParams' + export class Toolbar { instance: PostHog + + private _toolbarScriptLoaded = false + constructor(instance: PostHog) { this.instance = instance } @@ -98,8 +103,8 @@ export class Toolbar { } } } else { - // get credentials from localStorage from a previous initialzation - toolbarParams = JSON.parse(localStorage.getItem('_postHogToolbarParams') || '{}') + // get credentials from localStorage from a previous initialization + toolbarParams = JSON.parse(localStorage.getItem(LOCALSTORAGE_KEY) || '{}') toolbarParams.source = 'localstorage' // delete "add-action" or other intent from toolbarParams, otherwise we'll have the same intent @@ -118,22 +123,16 @@ export class Toolbar { } } + private _callLoadToolbar(params: ToolbarParams) { + ;(assignableWindow['ph_load_toolbar'] || assignableWindow['ph_load_editor'])(params, this.instance) + } + loadToolbar(params?: ToolbarParams): boolean { - if (!window || assignableWindow['_postHogToolbarLoaded']) { + if (!window || (window.localStorage.getItem(LOCALSTORAGE_KEY) && this._toolbarScriptLoaded)) { + // The toolbar will clear the localStorage key when it's done with it. If it is present that indicates the toolbar is already open and running return false } - // only load the toolbar once, even if there are multiple instances of PostHogLib - assignableWindow['_postHogToolbarLoaded'] = true - - // toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours. - // the toolbar asset includes a rotating "token" that is valid for 5 minutes. - const fiveMinutesInMillis = 5 * 60 * 1000 - // this ensures that we bust the cache periodically - const timestampToNearestFiveMinutes = Math.floor(Date.now() / fiveMinutesInMillis) * fiveMinutesInMillis - const toolbarUrl = this.instance.requestRouter.endpointFor( - 'assets', - `/static/toolbar.js?t=${timestampToNearestFiveMinutes}` - ) + const disableToolbarMetrics = this.instance.requestRouter.region === 'custom' && this.instance.config.advanced_disable_toolbar_metrics @@ -143,23 +142,47 @@ export class Toolbar { apiURL: this.instance.requestRouter.endpointFor('ui'), ...(disableToolbarMetrics ? { instrument: false } : {}), } + window.localStorage.setItem( + LOCALSTORAGE_KEY, + JSON.stringify({ + ...toolbarParams, + source: undefined, + }) + ) - const { source: _discard, ...paramsToPersist } = toolbarParams // eslint-disable-line - window.localStorage.setItem('_postHogToolbarParams', JSON.stringify(paramsToPersist)) + if (this._toolbarScriptLoaded) { + this._callLoadToolbar(toolbarParams) + } else { + // only load the toolbar once, even if there are multiple instances of PostHogLib + this._toolbarScriptLoaded = true + + // toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours. + // the toolbar asset includes a rotating "token" that is valid for 5 minutes. + const fiveMinutesInMillis = 5 * 60 * 1000 + // this ensures that we bust the cache periodically + const timestampToNearestFiveMinutes = Math.floor(Date.now() / fiveMinutesInMillis) * fiveMinutesInMillis + const toolbarUrl = this.instance.requestRouter.endpointFor( + 'assets', + `/static/toolbar.js?t=${timestampToNearestFiveMinutes}` + ) + + loadScript(toolbarUrl, (err) => { + if (err) { + logger.error('Failed to load toolbar', err) + this._toolbarScriptLoaded = false + return + } + this._callLoadToolbar(toolbarParams) + }) + + // Turbolinks doesn't fire an onload event but does replace the entire body, including the toolbar. + // Thus, we ensure the toolbar is only loaded inside the body, and then reloaded on turbolinks:load. + _register_event(window, 'turbolinks:load', () => { + this._toolbarScriptLoaded = false + this.loadToolbar(toolbarParams) + }) + } - loadScript(toolbarUrl, (err) => { - if (err) { - logger.error('Failed to load toolbar', err) - return - } - ;(assignableWindow['ph_load_toolbar'] || assignableWindow['ph_load_editor'])(toolbarParams, this.instance) - }) - // Turbolinks doesn't fire an onload event but does replace the entire body, including the toolbar. - // Thus, we ensure the toolbar is only loaded inside the body, and then reloaded on turbolinks:load. - _register_event(window, 'turbolinks:load', () => { - assignableWindow['_postHogToolbarLoaded'] = false - this.loadToolbar(toolbarParams) - }) return true }