diff --git a/playground/session-recordings/index.html b/playground/session-recordings/index.html index 97e514ff6..6a11a9de5 100644 --- a/playground/session-recordings/index.html +++ b/playground/session-recordings/index.html @@ -100,6 +100,23 @@ capture_performance: true, }) + posthog.init( + 'phc_other', + { + api_host: 'http://localhost:8000', + disable_session_recording: true, + capture_performance: true, + }, + 'other' + ) + + posthog.capture('event') + posthog.capture('event2') + posthog.people.set({ test: true }) + posthog.other.capture('other_event') + posthog.other.people.set({ test: true }) + console.log(posthog) + setTimeout(() => { posthog.debug() document.getElementById('current-session-id').innerHTML = posthog.sessionRecording.sessionId diff --git a/src/__tests__/posthog-core.identify.js b/src/__tests__/posthog-core.identify.js index 6d7d62b42..9efe54762 100644 --- a/src/__tests__/posthog-core.identify.js +++ b/src/__tests__/posthog-core.identify.js @@ -1,5 +1,6 @@ -import { PostHog } from '../posthog-core' +import _posthog from '../loader-module' import { PostHogPersistence } from '../posthog-persistence' +import { uuidv7 } from '../uuidv7' jest.mock('../gdpr-utils', () => ({ ...jest.requireActual('../gdpr-utils'), @@ -8,8 +9,7 @@ jest.mock('../gdpr-utils', () => ({ jest.mock('../decide') given('lib', () => { - const posthog = new PostHog() - posthog._init('testtoken', given.config, 'testhog') + const posthog = _posthog.init('testtoken', given.config, uuidv7()) return Object.assign(posthog, given.overrides) }) diff --git a/src/__tests__/posthog-core.js b/src/__tests__/posthog-core.js index 7db3a4f6c..db8694f61 100644 --- a/src/__tests__/posthog-core.js +++ b/src/__tests__/posthog-core.js @@ -1,11 +1,11 @@ -import { init_as_module, PostHog } from '../posthog-core' +import _posthog from '../loader-module' import { PostHogPersistence } from '../posthog-persistence' import { Decide } from '../decide' import { autocapture } from '../autocapture' -import { truth } from './helpers/truth' import { _info } from '../utils/event-utils' import { document, window } from '../utils/globals' +import { uuidv7 } from '../uuidv7' jest.mock('../gdpr-utils', () => ({ ...jest.requireActual('../gdpr-utils'), @@ -13,14 +13,17 @@ jest.mock('../gdpr-utils', () => ({ })) jest.mock('../decide') -given('lib', () => { - const posthog = new PostHog() - posthog._init('testtoken', given.config, 'testhog') - posthog.debug() - return Object.assign(posthog, given.overrides) -}) - describe('posthog core', () => { + given('lib', () => { + const posthog = _posthog.init('testtoken', given.config, uuidv7()) + posthog.debug() + return Object.assign(posthog, given.overrides) + }) + + afterEach(() => { + // Make sure there's no cached persistence + given.lib.persistence?.clear?.() + }) describe('capture()', () => { given('eventName', () => '$event') @@ -38,7 +41,6 @@ describe('posthog core', () => { given('overrides', () => ({ __loaded: true, - config: given.config, persistence: { remove_event_timer: jest.fn(), properties: jest.fn(), @@ -498,8 +500,6 @@ describe('posthog core', () => { }) describe('bootstrapping feature flags', () => { - given('subject', () => () => given.lib._init('posthog', given.config, 'testhog')) - given('overrides', () => ({ _send_request: jest.fn(), capture: jest.fn(), @@ -516,7 +516,6 @@ describe('posthog core', () => { }, })) - given.subject() expect(given.lib.get_distinct_id()).toBe('abcd') expect(given.lib.get_property('$device_id')).toBe('abcd') expect(given.lib.persistence.get_user_state()).toBe('anonymous') @@ -542,7 +541,6 @@ describe('posthog core', () => { get_device_id: () => 'og-device-id', })) - given.subject() expect(given.lib.get_distinct_id()).toBe('abcd') expect(given.lib.get_property('$device_id')).toBe('og-device-id') expect(given.lib.persistence.get_user_state()).toBe('identified') @@ -558,7 +556,6 @@ describe('posthog core', () => { }, })) - given.subject() expect(given.lib.get_distinct_id()).not.toBe('abcd') expect(given.lib.get_distinct_id()).not.toEqual(undefined) expect(given.lib.getFeatureFlag('multivariant')).toBe('variant-1') @@ -589,7 +586,6 @@ describe('posthog core', () => { }, })) - given.subject() expect(given.lib.getFeatureFlagPayload('multivariant')).toBe('some-payload') expect(given.lib.getFeatureFlagPayload('enabled')).toEqual({ another: 'value' }) expect(given.lib.getFeatureFlagPayload('jsonString')).toEqual({ a: 'payload' }) @@ -604,7 +600,6 @@ describe('posthog core', () => { bootstrap: {}, })) - given.subject() expect(given.lib.get_distinct_id()).not.toBe('abcd') expect(given.lib.get_distinct_id()).not.toEqual(undefined) expect(given.lib.getFeatureFlag('multivariant')).toBe(undefined) @@ -626,7 +621,6 @@ describe('posthog core', () => { }, })) - given.subject() given.lib.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(true) }) @@ -640,7 +634,6 @@ describe('posthog core', () => { }, })) - given.subject() given.lib.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(false) }) @@ -654,7 +647,6 @@ describe('posthog core', () => { }, })) - given.subject() given.lib.featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(false) }) @@ -662,7 +654,6 @@ describe('posthog core', () => { describe('init()', () => { jest.spyOn(window, 'window', 'get') - given('subject', () => () => given.lib._init('posthog', given.config, 'testhog')) given('overrides', () => ({ get_distinct_id: () => given.distinct_id, @@ -682,7 +673,6 @@ describe('posthog core', () => { given('advanced_disable_decide', () => true) it('can set an xhr error handler', () => { - init_as_module() const fakeOnXHRError = 'configured error' given('subject', () => given.lib.init( @@ -697,7 +687,6 @@ describe('posthog core', () => { }) it('does not load decide endpoint on advanced_disable_decide', () => { - given.subject() expect(given.decide).toBe(undefined) expect(given.overrides._send_request.mock.calls.length).toBe(0) // No outgoing requests }) @@ -706,44 +695,31 @@ describe('posthog core', () => { given('config', () => ({ api_host: 'https://example.com/custom/', })) - given.subject() expect(given.lib.config.api_host).toBe('https://example.com/custom') }) it('does not set __loaded_recorder_version flag if recording script has not been included', () => { - given('overrides', () => ({ - __loaded_recorder_version: undefined, - })) delete window.rrweb window.rrweb = { record: undefined } delete window.rrwebRecord window.rrwebRecord = undefined - given.subject() expect(given.lib.__loaded_recorder_version).toEqual(undefined) }) it('set __loaded_recorder_version flag to v1 if recording script has been included', () => { - given('overrides', () => ({ - __loaded_recorder_version: undefined, - })) delete window.rrweb window.rrweb = { record: 'anything', version: '1.1.3' } delete window.rrwebRecord window.rrwebRecord = 'is possible' - given.subject() expect(given.lib.__loaded_recorder_version).toMatch(/^1\./) // start with 1.?.? }) - it('set __loaded_recorder_version flag to v1 if recording script has been included', () => { - given('overrides', () => ({ - __loaded_recorder_version: undefined, - })) + it('set __loaded_recorder_version flag to v2 if recording script has been included', () => { delete window.rrweb window.rrweb = { record: 'anything', version: '2.0.0-alpha.6' } delete window.rrwebRecord window.rrwebRecord = 'is possible' - given.subject() expect(given.lib.__loaded_recorder_version).toMatch(/^2\./) // start with 2.?.? }) @@ -754,6 +730,7 @@ describe('posthog core', () => { startRecordingIfEnabled: jest.fn(), }, toolbar: { + maybeLoadToolbar: jest.fn(), afterDecideResponse: jest.fn(), }, persistence: { @@ -762,15 +739,11 @@ describe('posthog core', () => { }, })) - given.subject() - jest.spyOn(given.lib.toolbar, 'afterDecideResponse').mockImplementation() jest.spyOn(given.lib.sessionRecording, 'afterDecideResponse').mockImplementation() jest.spyOn(given.lib.persistence, 'register').mockImplementation() // Autocapture - expect(given.lib.__autocapture).toEqual(undefined) - expect(autocapture.init).not.toHaveBeenCalled() expect(autocapture.afterDecideResponse).not.toHaveBeenCalled() // Feature flags @@ -789,24 +762,22 @@ describe('posthog core', () => { describe('device id behavior', () => { it('sets a random UUID as distinct_id/$device_id if distinct_id is unset', () => { given('distinct_id', () => undefined) + given('config', () => ({ + get_device_id: (uuid) => uuid, + })) - given.subject() + expect(given.lib.persistence.props).toMatchObject({ + $device_id: expect.stringMatching(/^[0-9a-f-]+$/), + distinct_id: expect.stringMatching(/^[0-9a-f-]+$/), + }) - expect(given.lib.register_once).toHaveBeenCalledWith( - { - $device_id: truth((val) => val.match(/^[0-9a-f-]+$/)), - distinct_id: truth((val) => val.match(/^[0-9a-f-]+$/)), - }, - '' - ) + expect(given.lib.persistence.props.$device_id).toEqual(given.lib.persistence.props.distinct_id) }) it('does not set distinct_id/$device_id if distinct_id is unset', () => { given('distinct_id', () => 'existing-id') - given.subject() - - expect(given.lib.register_once).not.toHaveBeenCalled() + expect(given.lib.persistence.props.distinct_id).not.toEqual('existing-id') }) it('uses config.get_device_id for uuid generation if passed', () => { @@ -815,15 +786,10 @@ describe('posthog core', () => { get_device_id: (uuid) => 'custom-' + uuid.slice(0, 8), })) - given.subject() - - expect(given.lib.register_once).toHaveBeenCalledWith( - { - $device_id: truth((val) => val.match(/^custom-[0-9a-f]+/)), - distinct_id: truth((val) => val.match(/^custom-[0-9a-f]+/)), - }, - '' - ) + expect(given.lib.persistence.props).toMatchObject({ + $device_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), + distinct_id: expect.stringMatching(/^custom-[0-9a-f-]+$/), + }) }) }) }) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 55ea3f212..416018b8c 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -9,7 +9,7 @@ import { isCrossDomainCookie, isDistinctIdStringLike, } from './utils' -import { assignableWindow, window } from './utils/globals' +import { window } from './utils/globals' import { autocapture } from './autocapture' import { PostHogFeatureFlags } from './posthog-featureflags' import { PostHogPersistence } from './posthog-persistence' @@ -69,17 +69,7 @@ this.__x === private - only use within the class Globals should be all caps */ -enum InitType { - INIT_MODULE = 0, - INIT_SNIPPET = 1, -} - -let init_type: InitType - -// TODO: the type of this is very loose. Sometimes it's also PostHogLib itself -let posthog_master: Record & { - init: (token: string, config: Partial, name: string) => void -} +const instances: Record = {} // some globals for comparisons const __NOOP = () => {} @@ -163,93 +153,6 @@ export const defaultConfig = (): PostHogConfig => ({ session_idle_timeout_seconds: 30 * 60, // 30 minutes }) -/** - * create_phlib(token:string, config:object, name:string) - * - * This function is used by the init method of PostHogLib objects - * as well as the main initializer at the end of the JSLib (that - * initializes document.posthog as well as any additional instances - * declared before this file has loaded). - */ -const create_phlib = function ( - token: string, - config?: Partial, - name?: string, - createComplete?: (instance: PostHog) => void -): PostHog { - let instance: PostHog - const target = - name === PRIMARY_INSTANCE_NAME || !posthog_master ? posthog_master : name ? posthog_master[name] : undefined - const callbacksHandled = { - initComplete: false, - syncCode: false, - } - const handleCallback = (callbackName: keyof typeof callbacksHandled) => (instance: PostHog) => { - if (!callbacksHandled[callbackName]) { - callbacksHandled[callbackName] = true - if (callbacksHandled.initComplete && callbacksHandled.syncCode) { - createComplete?.(instance) - } - } - } - - if (target && init_type === InitType.INIT_MODULE) { - instance = target as any - } else { - if (target && !_isArray(target)) { - logger.error('You have already initialized ' + name) - // TODO: throw something instead? - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - return - } - instance = new PostHog() - } - - instance._init(token, config, name, handleCallback('initComplete')) - instance.toolbar.maybeLoadToolbar() - - instance.sessionRecording = new SessionRecording(instance) - instance.sessionRecording.startRecordingIfEnabled() - - if (instance.config.__preview_measure_pageview_stats) { - instance.pageViewManager.startMeasuringScrollPosition() - } - - instance.__autocapture = instance.config.autocapture - autocapture._setIsAutocaptureEnabled(instance) - if (autocapture._isAutocaptureEnabled) { - instance.__autocapture = instance.config.autocapture - const num_buckets = 100 - const num_enabled_buckets = 100 - if (!autocapture.enabledForProject(instance.config.token, num_buckets, num_enabled_buckets)) { - instance.__autocapture = false - logger.info('Not in active bucket: disabling Automatic Event Collection.') - } else if (!autocapture.isBrowserSupported()) { - instance.__autocapture = false - logger.info('Disabling Automatic Event Collection because this browser is not supported') - } else { - autocapture.init(instance) - } - } - - // if any instance on the page has debug = true, we set the - // global debug to be true - Config.DEBUG = Config.DEBUG || instance.config.debug - - // if target is not defined, we called init after the lib already - // loaded, so there won't be an array of things to execute - if (!_isUndefined(target) && _isArray(target)) { - // Crunch through the people queue first - we queue this data up & - // flush on identify, so it's better to do all these operations first - instance._execute_array.call(instance.people, (target as any).people) - instance._execute_array(target) - } - - handleCallback('syncCode')(instance) - return instance -} - class DeprecatedWebPerformanceObserver { get _forceAllowLocalhost(): boolean { return this.__forceAllowLocalhost @@ -363,24 +266,18 @@ export class PostHog { * @param {String} [name] The name for the new posthog instance that you want created */ init(token: string, config?: Partial, name?: string): PostHog | void { - if (_isUndefined(name)) { - logger.critical('You must name your new library: init(token, config, name)') - return - } - if (name === PRIMARY_INSTANCE_NAME) { - logger.critical( - 'You must initialize the main posthog object right after you include the PostHog js snippet' - ) - return - } - - const instance: PostHog = create_phlib(token, config, name, (instance: PostHog) => { - posthog_master[name] = instance - instance._loaded() - }) - posthog_master[name] = instance + if (!name || name === PRIMARY_INSTANCE_NAME) { + // This means we are initialising the primary instance (i.e. this) + return this._init(token, config, name) + } else { + const namedPosthog = instances[name] ?? new PostHog() + namedPosthog._init(token, config, name) + instances[name] = namedPosthog + // Add as a property to the primary instance (this isn't type-safe but its how it was always done) + ;(instances[PRIMARY_INSTANCE_NAME] as any)[name] = namedPosthog - return instance + return namedPosthog + } } // posthog._init(token:string, config:object, name:string) @@ -396,34 +293,23 @@ export class PostHog { // IE11 compatible. We could use polyfills, which would make the // code a bit cleaner, but will add some overhead. // - _init( - token: string, - config: Partial = {}, - name?: string, - initComplete?: (instance: PostHog) => void - ): void { + _init(token: string, config: Partial = {}, name?: string): PostHog | void { + if (!token) { + logger.critical( + 'PostHog was initialized without a token. This likely indicates a misconfiguration. Please check the first argument passed to posthog.init()' + ) + return + } + + if (this.__loaded) { + logger.warn('You have already initialized PostHog! Re-initialising is a no-op') + return + } + this.__loaded = true this.config = {} as PostHogConfig // will be set right below this._triggered_notifs = [] - // To avoid using Promises and their helper functions, we instead keep - // track of which callbacks have been called, and then call initComplete - // when all of them have been called. To add additional async code, add - // to `callbacksHandled` and pass updateInitComplete as a callback to - // the async code. - const callbacksHandled = { segmentRegister: false, syncCode: false } - const updateInitComplete = (callbackName: keyof typeof callbacksHandled) => () => { - // Update the register of callbacks that have been called, and if - // they have all been called, then we are ready to call - // initComplete. - if (!callbacksHandled[callbackName]) { - callbacksHandled[callbackName] = true - if (callbacksHandled.segmentRegister && callbacksHandled.syncCode) { - initComplete?.(this) - } - } - } - this.set_config( _extend({}, defaultConfig(), config, { name: name, @@ -457,6 +343,37 @@ export class PostHog { ? this.persistence : new PostHogPersistence({ ...this.config, persistence: 'sessionStorage' }) + this.sessionRecording = new SessionRecording(this) + this.sessionRecording.startRecordingIfEnabled() + + this.sessionRecording = new SessionRecording(this) + this.sessionRecording.startRecordingIfEnabled() + + if (this.config.__preview_measure_pageview_stats) { + this.pageViewManager.startMeasuringScrollPosition() + } + + this.__autocapture = this.config.autocapture + autocapture._setIsAutocaptureEnabled(this) + if (autocapture._isAutocaptureEnabled) { + this.__autocapture = this.config.autocapture + const num_buckets = 100 + const num_enabled_buckets = 100 + if (!autocapture.enabledForProject(this.config.token, num_buckets, num_enabled_buckets)) { + this.__autocapture = false + logger.info('Not in active bucket: disabling Automatic Event Collection.') + } else if (!autocapture.isBrowserSupported()) { + this.__autocapture = false + logger.info('Disabling Automatic Event Collection because this browser is not supported') + } else { + autocapture.init(this) + } + } + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || this.config.debug + this._gdpr_init() if (config.segment) { @@ -470,10 +387,6 @@ export class PostHog { }) this.persistence.set_user_state('identified') } - - config.segment.register(this.segmentIntegration()).then(updateInitComplete('segmentRegister')) - } else { - updateInitComplete('segmentRegister')() } // isUndefined doesn't provide typehint here so wouldn't reduce bundle as we'd need to assign @@ -514,6 +427,7 @@ export class PostHog { // or the device id if something was already stored // in the persitence const uuid = this.config.get_device_id(uuidv7()) + this.register_once( { distinct_id: uuid, @@ -528,13 +442,21 @@ export class PostHog { // Use `onpagehide` if available, see https://calendar.perfplanet.com/2020/beaconing-in-practice/#beaconing-reliability-avoiding-abandons window?.addEventListener?.('onpagehide' in self ? 'pagehide' : 'unload', this._handle_unload.bind(this)) - // Make sure that we also call the initComplete callback at the end of - // the synchronous code as well. - updateInitComplete('syncCode')() + this.toolbar.maybeLoadToolbar() + + // We wan't to avoid promises for IE11 compatibility, so we use callbacks here + if (config.segment) { + config.segment.register(this.segmentIntegration()).then(() => { + this._loaded() + }) + } else { + this._loaded() + } + + return this } // Private methods - _afterDecideResponse(response: DecideResponse) { this.compression = {} if (response.supportedCompression && !this.config.disable_compression) { @@ -1796,7 +1718,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] } /** @@ -1819,7 +1741,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 { @@ -2084,59 +2006,6 @@ export class PostHog { _safewrap_class(PostHog, ['identify']) -const instances: Record = {} -const extend_mp = function () { - // add all the sub posthog instances - _each(instances, function (instance, name) { - if (name !== PRIMARY_INSTANCE_NAME) { - posthog_master[name] = instance - } - }) -} - -const override_ph_init_func = function () { - // we override the snippets init function to handle the case where a - // user initializes the posthog library after the script loads & runs - posthog_master['init'] = function (token?: string, config?: Partial, name?: string) { - if (name) { - // initialize a sub library - if (!posthog_master[name]) { - posthog_master[name] = instances[name] = create_phlib( - token || '', - config || {}, - name, - (instance: PostHog) => { - posthog_master[name] = instances[name] = instance - instance._loaded() - } - ) - } - return posthog_master[name] - } else { - let instance: PostHog = posthog_master as any as PostHog - - if (instances[PRIMARY_INSTANCE_NAME]) { - // main posthog lib already initialized - instance = instances[PRIMARY_INSTANCE_NAME] - } else if (token) { - // intialize the main posthog lib - instance = create_phlib(token, config || {}, PRIMARY_INSTANCE_NAME, (instance: PostHog) => { - instances[PRIMARY_INSTANCE_NAME] = instance - instance._loaded() - }) - instances[PRIMARY_INSTANCE_NAME] = instance - } - - ;(posthog_master as any) = instance - if (init_type === InitType.INIT_SNIPPET) { - assignableWindow[PRIMARY_INSTANCE_NAME] = posthog_master - } - extend_mp() - return instance - } - } -} - const add_dom_loaded_handler = function () { // Cross browser DOM Loaded support function dom_loaded_handler() { @@ -2172,43 +2041,68 @@ const add_dom_loaded_handler = function () { } export function init_from_snippet(): void { - init_type = InitType.INIT_SNIPPET - if (_isUndefined(assignableWindow.posthog)) { - assignableWindow.posthog = [] - } - posthog_master = assignableWindow.posthog - - if (posthog_master['__loaded'] || (posthog_master['config'] && posthog_master['persistence'])) { - // lib has already been loaded at least once; we don't want to override the global object this time so bomb early - logger.critical('PostHog library has already been downloaded at least once.') - return + const posthogMaster = (instances[PRIMARY_INSTANCE_NAME] = new PostHog()) + + const snippetPostHog = (window as any)['posthog'] + + if (snippetPostHog) { + /** + * The snippet uses some clever tricks to allow deferred loading of array.js (this code) + * + * window.posthog is an array which the queue of calls made before the lib is loaded + * It has methods attached to it to simulate the posthog object so for instance + * + * window.posthog.init("TOKEN", {api_host: "foo" }) + * window.posthog.capture("my-event", {foo: "bar" }) + * + * ... will mean that window.posthog will look like this: + * window.posthog == [ + * ["my-event", {foo: "bar"}] + * ] + * + * window.posthog[_i] == [ + * ["TOKEN", {api_host: "foo" }, "posthog"] + * ] + * + * If a name is given to the init function then the same as above is true but as a sub-property on the object: + * + * window.posthog.init("TOKEN", {}, "ph2") + * window.posthog.ph2.people.set({foo: "bar"}) + * + * window.posthog.ph2 == [] + * window.posthog.people == [ + * ["set", {foo: "bar"}] + * ] + * + */ + + // Call all pre-loaded init calls properly + + _each(snippetPostHog['_i'], function (item: [token: string, config: Partial, name: string]) { + if (item && _isArray(item)) { + const instance = posthogMaster.init(item[0], item[1], item[2]) + + const instanceSnippet = snippetPostHog[item[2]] || snippetPostHog + + if (instance) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance.people, instanceSnippet.people) + instance._execute_array(instanceSnippet) + } + } + }) } - // Load instances of the PostHog Library - _each(posthog_master['_i'], function (item: [token: string, config: Partial, name: string]) { - if (item && _isArray(item)) { - instances[item[2]] = create_phlib(...item) - } - }) - - override_ph_init_func() - ;(posthog_master['init'] as any)() - - // Fire loaded events after updating the window's posthog object - _each(instances, function (instance) { - instance._loaded() - }) + ;(window as any)['posthog'] = posthogMaster add_dom_loaded_handler() } export function init_as_module(): PostHog { - init_type = InitType.INIT_MODULE - ;(posthog_master as any) = new PostHog() + const posthogMaster = (instances[PRIMARY_INSTANCE_NAME] = new PostHog()) - override_ph_init_func() - ;(posthog_master['init'] as any)() add_dom_loaded_handler() - return posthog_master as any + return posthogMaster }