Skip to content

Removing the legacy PostHog instance ​

The widget currently tracks to two PostHog instances in parallel:

Legacy instanceNew instance
Packageposthog-js (^1.109.0, locked to 1.399.5)posthog-js-v2 (npm alias for posthog-js@1.418.5)
Servicesrc/GlobalServices/InternalAnalytics/PosthogAnalytics.tssrc/GlobalServices/InternalAnalytics/PosthogV2Analytics.ts
ConfigVITE_POSTHOG_PROJECT / VITE_POSTHOG_HOST, always initialisedVITE_POSTHOG_V2_PROJECT / VITE_POSTHOG_V2_HOST, initialised only when both are set
LoadingBundled statically into bookingkitWidgetElements.jsLazily imported chunk, fetched only when configured
Handlewindow.bookingkitServices.internalAnalyticsServicewindow.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_PROJECT and VITE_POSTHOG_V2_HOST are set in .env.stage and .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.internalAnalyticsServiceV2 by that name (search partner snippets / internal tooling). After the migration only internalAnalyticsService exists.
  • [ ] Work on a dedicated branch; the change touches dependencies, env files, source, unit tests, E2E tests and docs.

Step 1 — Dependencies ​

In package.json:

  1. Remove the legacy line "posthog-js": "^1.109.0".
  2. 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).
  3. Delete the whole overrides → posthog-js block. It only exists to stop npm from hoisting newer @posthog/core, @posthog/types and dompurify under 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 each

Step 2 — Source files ​

All paths are relative to src/GlobalServices/InternalAnalytics/.

ActionFile
DeletePosthogAnalytics.ts (legacy class)
DeletePosthogAnalytics.spec.ts (legacy spec)
RenamePosthogV2Analytics.ts → PosthogAnalytics.ts
RenamePosthogV2Analytics.spec.ts → PosthogAnalytics.spec.ts
RenameposthogV2Options.ts → posthogOptions.ts
RenameposthogV2Options.spec.ts → posthogOptions.spec.ts
Renameidentifiers PosthogV2Bootstrap → PosthogBootstrap and buildPosthogV2Bootstrap → buildPosthogBootstrap inside posthogOptions.ts
RenameposthogV2PageContext.ts → posthogPageContext.ts (and its spec); identifiers PosthogV2PageContext → PosthogPageContext, POSTHOG_V2_* constants → POSTHOG_*
KeepinsightsEventSubscriptions.ts and its spec

Inside the renamed files:

  • import('posthog-js-v2') → import('posthog-js'), and every import 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.ts remove the "verbatim copy of PosthogAnalytics.…" comments; it is now the only implementation.
  • In insightsEventSubscriptions.spec.ts delete the mapNavigationChangedEvent parity with PosthogAnalytics block and its imports (posthog, PosthogAnalytics). The remaining tests cover the subscriptions directly.
  • Revisit POSTHOG_OPTIONS. disable_external_dependency_loading, disable_session_recording and disable_surveys were added to keep two posthog-js copies from fighting over window.__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: false and capture_pageleave: false must stay (they are the legacy behaviour). The identity options must stay as well: person_profiles: 'never' and persistence: 'memory', together with the bootstrap built from the promise returned by sessionService.getSessionPrefix() (init waits for it) and the register({ vendorIdent }) call in identify(). Keep the $set / $set_once → register / register_once translation in trackEvent() too: without person profiles it is the only way person properties such as affiliateLink reach the events. Keep the virtual page context as well (posthogV2PageContext.ts): it rewrites $current_url / $host / $pathname to https://widget.bookingkit.com/widget/<widgetType>/<page>, keeps the customer page as embed_url / embed_host, and maps page_view to $pageview, which is what makes cross-customer reports possible. The new instance never calls posthog.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 ​

  1. Remove the legacy postHogConfig object and the new PosthogAnalytics({...}) call that uses it, together with the PosthogAnalytics import of the legacy class.
  2. Rename postHogV2Config → postHogConfig and internalAnalyticsServiceV2 → internalAnalyticsService; update the imports to the renamed modules.
  3. In the declare global { interface Window { bookingkitServices: { … } } } block delete internalAnalyticsServiceV2?: IInsights; and make the remaining entry internalAnalyticsService?: IInsights; (optional, because it is undefined when PostHog is not configured).
  4. In the window.bookingkitServices = { … } object remove internalAnalyticsServiceV2.
  5. 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 old VITE_POSTHOG_PROJECT / VITE_POSTHOG_HOST lines, then rename the _V2_ lines. .env.featureTest is a symlink to .env.stage and 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, token phc_e2e_fake). The E2E build must never point at a real PostHog host.
  • src/vite-env.d.ts: rename the two ImportMetaEnv members.
  • Any CI variables or deployment secrets that were created for the _V2_ names.

Step 5 — E2E tests ​

ActionFile
Deletee2e/Tests/SmokeTests/Tracking/Posthog.spec.ts (legacy, asserts on the fakeDataLayer override)
Renamee2e/Tests/SmokeTests/Tracking/PosthogV2.spec.ts → Posthog.spec.ts
Mergee2e/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.
Renamee2e/Utilities/Tracking/Posthog/PosthogV2Requests.ts → PosthogRequests.ts; rename collectPosthogV2Events → collectPosthogEvents, isPosthogV2CaptureRequest → isPosthogCaptureRequest, PosthogV2Collector → PosthogCollector.
Simplifye2e/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.
Simplifye2e/WidgetPages/index.ts: call the single interceptPostHogLoadings once in visitPage().
Clean upe2e/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.
Configplaywright.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 when navigator.webdriver is true, and the widget must not disable that filter in production.
  • Remove the "existing instance still received … events" steps and the DATA_LAYER_LENGTH / getFakeDataLayer imports.
  • 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 (single Posthog.spec.ts, PosthogTestData.ts, Posthog/PosthogRequests.ts, single interceptor).
  • CLAUDE.md → Test Organization: list only Posthog.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.ts

Finally, 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