Appearance
Removing the legacy PostHog instance
The widget currently tracks to two PostHog instances in parallel:
| Legacy instance | New instance | |
|---|---|---|
| Package | posthog-js (^1.109.0, locked to 1.399.5) | posthog-js-v2 (npm alias for posthog-js@1.418.5) |
| Service | src/GlobalServices/InternalAnalytics/PosthogAnalytics.ts | src/GlobalServices/InternalAnalytics/PosthogV2Analytics.ts |
| Config | VITE_POSTHOG_PROJECT / VITE_POSTHOG_HOST, always initialised | VITE_POSTHOG_V2_PROJECT / VITE_POSTHOG_V2_HOST, initialised only when both are set |
| Loading | Bundled statically into bookingkitWidgetElements.js | Lazily imported chunk, fetched only when configured |
| Handle | window.bookingkitServices.internalAnalyticsService | window.bookingkitServices.internalAnalyticsServiceV2 |
The event wiring of the new instance (insightsEventSubscriptions.ts) is a verbatim copy of the legacy listen() / mapNavigationChangedEvent() logic and is parity-tested against the legacy class. This is deliberate: it lets the legacy class be deleted without touching behaviour.
This document describes how to remove the legacy instance once the new PostHog project is the source of truth. The end state has no V2 / v2 suffix anywhere: the new implementation simply takes over the old names.
Before you start
- [ ]
VITE_POSTHOG_V2_PROJECTandVITE_POSTHOG_V2_HOSTare set in.env.stageand.env.production, and the new project has been receiving production events long enough for dashboards, insights and alerts to have been migrated. - [ ] Nobody depends on
window.bookingkitServices.internalAnalyticsServiceV2by that name (search partner snippets / internal tooling). After the migration onlyinternalAnalyticsServiceexists. - [ ] Work on a dedicated branch; the change touches dependencies, env files, source, unit tests, E2E tests and docs.
Step 1 — Dependencies
In package.json:
- Remove the legacy line
"posthog-js": "^1.109.0". - Rename the alias
"posthog-js-v2": "npm:posthog-js@1.418.5"to a normal pinned dependency:"posthog-js": "1.418.5"(or whatever version is current at that point). - Delete the whole
overrides→posthog-jsblock. It only exists to stop npm from hoisting newer@posthog/core,@posthog/typesanddompurifyunder the legacy package.
Then:
bash
npm install
node -p "require('posthog-js/package.json').version" # expect the pinned version
npm ls @posthog/core @posthog/types dompurify # expect exactly one version eachStep 2 — Source files
All paths are relative to src/GlobalServices/InternalAnalytics/.
| Action | File |
|---|---|
| Delete | PosthogAnalytics.ts (legacy class) |
| Delete | PosthogAnalytics.spec.ts (legacy spec) |
| Rename | PosthogV2Analytics.ts → PosthogAnalytics.ts |
| Rename | PosthogV2Analytics.spec.ts → PosthogAnalytics.spec.ts |
| Rename | posthogV2Options.ts → posthogOptions.ts |
| Rename | posthogV2Options.spec.ts → posthogOptions.spec.ts |
| Rename | identifiers PosthogV2Bootstrap → PosthogBootstrap and buildPosthogV2Bootstrap → buildPosthogBootstrap inside posthogOptions.ts |
| Rename | posthogV2PageContext.ts → posthogPageContext.ts (and its spec); identifiers PosthogV2PageContext → PosthogPageContext, POSTHOG_V2_* constants → POSTHOG_* |
| Keep | insightsEventSubscriptions.ts and its spec |
Inside the renamed files:
import('posthog-js-v2')→import('posthog-js'), and everyimport type { … } from 'posthog-js-v2'→'posthog-js'.jest.mock('posthog-js-v2', …)→jest.mock('posthog-js', …).- Rename identifiers, dropping the suffix:
PosthogV2Analytics→PosthogAnalytics,PosthogV2Client→PosthogClient,PosthogV2Module→PosthogModule,PosthogV2Loader→PosthogLoader,loadPosthogV2→loadPosthog,PosthogV2Config→PosthogConfig,POSTHOG_V2_ISOLATION_OPTIONS→POSTHOG_OPTIONS,isPosthogV2Configured→isPosthogConfigured,buildPosthogV2Config→buildPosthogConfig. - In
insightsEventSubscriptions.tsremove the "verbatim copy ofPosthogAnalytics.…" comments; it is now the only implementation. - In
insightsEventSubscriptions.spec.tsdelete themapNavigationChangedEvent parity with PosthogAnalyticsblock and its imports (posthog,PosthogAnalytics). The remaining tests cover the subscriptions directly. - Revisit
POSTHOG_OPTIONS.disable_external_dependency_loading,disable_session_recordinganddisable_surveyswere added to keep two posthog-js copies from fighting overwindow.__PosthogExtensions__. With a single copy they are no longer required for isolation; keep them only if the product still does not want those features.autocapture: false,capture_pageview: falseandcapture_pageleave: falsemust stay (they are the legacy behaviour). The identity options must stay as well:person_profiles: 'never'andpersistence: 'memory', together with thebootstrapbuilt from the promise returned bysessionService.getSessionPrefix()(init waits for it) and theregister({ vendorIdent })call inidentify(). Keep the$set/$set_once→register/register_oncetranslation intrackEvent()too: without person profiles it is the only way person properties such asaffiliateLinkreach the events. Keep the virtual page context as well (posthogV2PageContext.ts): it rewrites$current_url/$host/$pathnametohttps://widget.bookingkit.com/widget/<widgetType>/<page>, keeps the customer page asembed_url/embed_host, and mapspage_viewto$pageview, which is what makes cross-customer reports possible. The new instance never callsposthog.identify; the distinct id is the session prefix (session id without the server hash) shared with the checkout app.
Decide whether to keep the lazy import and env gate. Recommended: keep both. The chunk is only fetched when PostHog is configured, which keeps local and preview builds silent, and the queue-until-ready logic already covers events emitted before the chunk arrives.
Step 3 — Wiring in src/GlobalServices/index.ts
- Remove the legacy
postHogConfigobject and thenew PosthogAnalytics({...})call that uses it, together with thePosthogAnalyticsimport of the legacy class. - Rename
postHogV2Config→postHogConfigandinternalAnalyticsServiceV2→internalAnalyticsService; update the imports to the renamed modules. - In the
declare global { interface Window { bookingkitServices: { … } } }block deleteinternalAnalyticsServiceV2?: IInsights;and make the remaining entryinternalAnalyticsService?: IInsights;(optional, because it isundefinedwhen PostHog is not configured). - In the
window.bookingkitServices = { … }object removeinternalAnalyticsServiceV2. - Replace the "Temporary second PostHog instance" comment with a one-liner explaining the env gate.
Step 4 — Environment variables
Rename VITE_POSTHOG_V2_PROJECT → VITE_POSTHOG_PROJECT and VITE_POSTHOG_V2_HOST → VITE_POSTHOG_HOST, and drop the legacy values:
.env.example,.env.stage,.env.production, and your local.env: delete the oldVITE_POSTHOG_PROJECT/VITE_POSTHOG_HOSTlines, then rename the_V2_lines..env.featureTestis a symlink to.env.stageand needs no change..env.e2e: same rename. Keep the fake host and token (https://posthog-v2.bookingkit.test→ rename to e.g.https://posthog.bookingkit.test, tokenphc_e2e_fake). The E2E build must never point at a real PostHog host.src/vite-env.d.ts: rename the twoImportMetaEnvmembers.- Any CI variables or deployment secrets that were created for the
_V2_names.
Step 5 — E2E tests
| Action | File |
|---|---|
| Delete | e2e/Tests/SmokeTests/Tracking/Posthog.spec.ts (legacy, asserts on the fakeDataLayer override) |
| Rename | e2e/Tests/SmokeTests/Tracking/PosthogV2.spec.ts → Posthog.spec.ts |
| Merge | e2e/Utilities/TestData/PosthogV2TestData.ts into PosthogTestData.ts, dropping the _V2 prefix of every constant (POSTHOG_HOST, POSTHOG_URL_PATTERN, POSTHOG_CAPTURE_PATH, POSTHOG_PROJECT_TOKEN, POSTHOG_LIB_VERSION, POSTHOG_EXPECTED_EVENTS, POSTHOG_FLUSH_TIMEOUT_MS). Update POSTHOG_HOST / POSTHOG_PROJECT_TOKEN to the new .env.e2e values and POSTHOG_LIB_VERSION to the pinned version. |
| Rename | e2e/Utilities/Tracking/Posthog/PosthogV2Requests.ts → PosthogRequests.ts; rename collectPosthogV2Events → collectPosthogEvents, isPosthogV2CaptureRequest → isPosthogCaptureRequest, PosthogV2Collector → PosthogCollector. |
| Simplify | e2e/Utilities/Interceptors/Analytics/Posthog.ts: delete the legacy POSTHOG_URL_PATTERN route (the one that answers with window.posthog = {capture: () => {}}), rename interceptPostHogV2Loadings → interceptPostHogLoadings and keep the GetWidgetSessionId route. |
| Simplify | e2e/WidgetPages/index.ts: call the single interceptPostHogLoadings once in visitPage(). |
| Clean up | e2e/WidgetPages/PageWithPHTracking/index.html: remove the window.fakeDataLayer / trackEvent override; the spec asserts on network traffic instead. Then remove getFakeDataLayer from e2e/Utilities/Tracking/tracking.ts and the fakeDataLayer fallback in getDataLayer if nothing else uses them. |
| Config | playwright.config.ts: the ph-tracking-page project goes back to testMatch: 'Posthog.spec.ts'. |
In the renamed spec:
- Keep the
hideAutomationMarkersFromPosthog()step. posthog-js drops every capture whennavigator.webdriveris true, and the widget must not disable that filter in production. - Remove the "existing instance still received … events" steps and the
DATA_LAYER_LENGTH/getFakeDataLayerimports. - Rename the describe block and test title (no "V2", no "dual tracking").
Step 6 — Docs and project files
docs/e2e-testing.md: update the directory tree (singlePosthog.spec.ts,PosthogTestData.ts,Posthog/PosthogRequests.ts, single interceptor).CLAUDE.md→ Test Organization: list onlyPosthog.spec.ts.- Delete this page and its entry in
docs/.vitepress/config.ts.
Step 7 — Verify
bash
# 1. No suffix left anywhere (expect no output)
grep -rniE "posthog[-_]?v2|POSTHOG_V2|internalAnalyticsServiceV2" \
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=test-results \
--exclude-dir=playwright-report --exclude-dir=.git .
# 2. Exactly one posthog-js in the tree, at the pinned version
npm ls posthog-js @posthog/core @posthog/types
# 3. Unit tests
npm run test:unit
# 4. Build and confirm PostHog lives in its own lazily loaded chunk
npm run build:e2e
grep -l "$(node -p "require('posthog-js/package.json').version")" dist/assets/ce/*.js
grep -c "$(node -p "require('posthog-js/package.json').version")" dist/bookingkitWidgetElements.js # expect 0
# 5. E2E (tracking project plus the two projects that share the PostHog page fixture)
BROWSER_NAME='Desktop Chrome' npx playwright test \
--project="ph-tracking-page [Desktop Chrome]" \
--project="with-post-hog-page [Desktop Chrome]" \
--project="single-widget-page [Desktop Chrome]"
# 6. Lint
npx eslint e2e playwright.config.tsFinally, load a page with the widget in a real browser with PostHog unconfigured and confirm no PostHog chunk or request is made, then with PostHog configured and confirm POST <host>/e/ batches arrive with the new $lib_version.
Expected end state
package.json posthog-js pinned, no alias, no overrides
src/GlobalServices/
index.ts one PostHog service, env-gated, exposed as internalAnalyticsService
InternalAnalytics/
PosthogAnalytics.ts lazy import + queue-until-ready (formerly PosthogV2Analytics.ts)
PosthogAnalytics.spec.ts
posthogOptions.ts env gate + init options (formerly posthogV2Options.ts)
posthogOptions.spec.ts
posthogPageContext.ts virtual widget URLs, embed_url, page_view → $pageview (formerly posthogV2PageContext.ts)
posthogPageContext.spec.ts
insightsEventSubscriptions.ts event-bus wiring, single implementation
insightsEventSubscriptions.spec.ts
e2e/
Tests/SmokeTests/Tracking/Posthog.spec.ts network-level assertions
Utilities/TestData/PosthogTestData.ts
Utilities/Tracking/Posthog/PosthogRequests.ts
Utilities/Interceptors/Analytics/Posthog.ts one PostHog route + GetWidgetSessionId
.env* VITE_POSTHOG_PROJECT / VITE_POSTHOG_HOST only