Skip to content

feat(analytics): load Segment only with Transcend consent - #5340

Draft
luismoriguerra wants to merge 4 commits into
mainfrom
feat/segment-consent
Draft

luismoriguerra wants to merge 4 commits into
mainfrom
feat/segment-consent

Conversation

@luismoriguerra

@luismoriguerra luismoriguerra commented Nov 13, 2026 •

Copy link
Copy Markdown

Summary

The CDP frontend loaded Segment with a custom snippet stub (frontend/src/utils/segment.ts) that injected https://cdn.segment.com/analytics.js/v1/<key>/analytics.min.js straight from src/main.ts, with no consent check. Transcend's airgap.js is already the first, synchronous script in index.html (#4969), but nothing passed the visitor's choice to Segment, and Segment loaded whatever the choice.

This PR switches the frontend to the LFX Segments library (https://lfx-segment.platform.linuxfoundation.org/latest/lfx-segment-analytics.min.js, built from linuxfoundation/segment-web-scripts). That library is the Transcend consent wrapper from the Segment Consent Management epic (linuxfoundation/lfx-self-serve-ops#1141), and every other LFX property already loads it. From its consent release (v0.0.14, due on latest/ on 2026-11-23; see Open questions):

  • no window.airgap (blocked, missing, or late by more than 10 s): Segment never loads;
  • no Analytics consent: Segment never loads; withdrawn later: sending stops and the stored Segment identity is cleared;
  • opt-in regimes (everything except US_DNSS): waits for a confirmed choice, then loads on a later grant and sends the page view;
  • every event carries context.consent.categoryPreferences, and each later change sends Segment Consent Preference Updated;
  • ad click IDs are stripped from events without Advertising consent.

Why the library instead of wrapping the stub with withTranscend

Every call site uses only track(event, properties), page(name) and identify(userId, traits), which the library exposes one-to-one. Wrapping the stub would have meant a second copy of the regime rules, the withdrawal clean-up and the click-ID stripping in this repo, kept in sync by hand. With the library, one implementation covers every LFX property, and a fix there reaches CDP through latest/.

The write key is unchanged: init(VUE_APP_SEGMENT_KEY) passes CDP's own key, so events keep going to the same Segment source.

Changes

  • frontend/src/utils/segment.ts: the stub is replaced by a loader that injects the LFX library from latest/ and calls init(writeKey), plus an analytics module (track, page, identify). Calls made while the library loads are forwarded once it has loaded. The library drops calls made while Segment is held for consent; they are not replayed after a later grant. A blocked library script makes every call a no-op, silently. A library that loads without LfxSegmentsAnalytics is logged (Segment library loaded without LfxSegmentsAnalytics). A call that throws or rejects is logged (Segment call failed) instead of becoming an unhandled rejection, because no call site awaits or catches it.
  • init() is called before any queued call reaches the instance, and its promise is not awaited. A failed first load is retried by the library on the next consent change, so later calls must still go through.
  • The 11 window.analytics call sites (identify.ts, saved views, member/organization/activity pages, the LinkedIn/Reddit/Stack Overflow drawers, the paywall page) now import analytics from @/utils/segment. The Window.analytics global and its any casts and eslint-disable comments are gone.
  • src/main.ts: drops installAnalyticsStub(). Segment still loads only when VUE_APP_SEGMENT_KEY is a real key.
  • Sign-out: Auth0Service.logout(), which every sign-out path reaches (the user's logout, a failed /me at boot, a 401), calls analytics.reset() before the Auth0 redirect. The library clears the stored Segment user ID and traits and its cross-domain link IDs. It isn't awaited, so sign-out never waits on analytics.
  • Unit tests: vitest 4.1.11 (the version the root already uses) is added to the frontend, with frontend/vitest.config.ts (resolves the @ alias, type-checked through tsconfig.json), a test script, and a "Unit tests" step in frontend-checks.yml. The lockfile change is just that entry. eslint.config.mjs allows devDependencies in src/**/*.test.ts and vitest.config.ts.
  • Docs: frontend/README.md (Segment needs Transcend Analytics consent; the test script; what CI runs), frontend/CLAUDE.md (pnpm run test in the checks), frontend/docs/environment.md (VUE_APP_SEGMENT_KEY is read in config.ts).

Behaviour changes to be aware of:

  • The library sends one unnamed page view when Segment starts (with domain), in addition to the named page('Members'), page('Organization') and page('Activities') calls.
  • It also runs the LFX cross-domain identity link (lfajs_aid on links to LFX domains, removed from the address bar), as on other LFX sites.
  • Sign-out now clears the Segment identity, and the next anonymous ID is new.
  • In opt-in regimes, the boot identify is dropped while Segment waits for a choice and, with the library as released, isn't replayed after the grant: events after a mid-session grant stay anonymous until the next full load. The fix is in the library: linuxfoundation/segment-web-scripts#73 (draft) holds the latest identify and sends it on grant, as it already does for identifyAuth0User. CDP needs no change when it ships through latest/.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Performance improvement
  • Chore / dependency update
  • Documentation

Related issue

Closes linuxfoundation/lfx-self-serve-ops#1148

Part of linuxfoundation/lfx-self-serve-ops#1141.

Investigation

  • Segment call sites: window.analytics.identify in shared/modules/monitoring/identify.ts; .page in the member, organization and activity list pages; .track in SavedViewManagement.vue, SavedViewForm.vue, member-list-emails.vue, temporary-paywall-page.vue and the LinkedIn/Reddit/Stack Overflow settings drawers. No reset, group or alias calls.
  • airgap: frontend/index.html loads airgap.js as the first <script> in <head>, synchronously, since feat: replace osano with transcend cookie consent #4969. The LFX library is injected later, from src/main.ts, which is a module script at the end of <body>. So airgap always runs first. feat: replace osano with transcend cookie consent #4969 is live on both hosts: the frontend production deploy of 2026-11-11 (commit 8bd10f1) includes it, and on 2026-11-13 a headless Chrome check of https://cm.lfx.dev/ and https://lf-staging.crowd.dev/ found window.airgap running (regime US_DNSS from a US address). Both hosts still load Segment through the old stub.
  • Hosts: production cm.lfx.dev, staging lf-staging.crowd.dev (the backend OpenAPI servers and ADR-0016). Neither sends a Content-Security-Policy header (nginx.kube.conf sets none), so no CSP change is needed for transcend-cdn.com or lfx-segment.platform.linuxfoundation.org.
  • Write key: VUE_APP_SEGMENT_KEY. The build has the placeholder CROWD_VUE_APP_SEGMENT_KEY, which scripts/docker-entrypoint.sh replaces when the container starts. Production has a real key configured. Staging has none: today's staging stub requests analytics.js/v1//analytics.min.js, and with this PR isRealSegmentKey('') keeps Segment off there.
  • LFX Insights: no vendored copy. Its crowd.dev submodule is a sparse checkout of services/libs/*, services/archetypes and services/base.tsconfig.json (init-submodules.sh), so frontend/index.html is not in it. analytics.load appears nowhere in linuxfoundation/insights. Insights loads the LFX library through lfxSegmentCdnUrl (frontend/app/plugins/analytics.ts).

How it was verified

  • pnpm run lint (0 warnings), pnpm run lint:cycles (0 new), pnpm run typecheck (0 errors before and after), pnpm run test (9 passing), and pnpm run build:production all pass, run from frontend/.

  • Unit tests (src/utils/segment.test.ts) cover:

    • placeholder keys count as no key, and with no key nothing is injected or sent;
    • the loader injects the latest/ library, not the Segment CDN;
    • init(writeKey) runs before the calls made while the library loaded (page, track, identify, reset), which are then forwarded in order, with their properties and traits;
    • calls still go through after a failed first init();
    • a call that throws is contained and logged;
    • a library without LfxSegmentsAnalytics is logged and sends nothing;
    • a blocked library script sends nothing;
    • index.html loads airgap.js as the first script, without async, defer or type="module", matched case-insensitively.
  • CDP's own calls through the real library, in headless Chrome: segment.ts bundled into a test page with a fake airgap, the library served as latest/, Segment settings and API intercepted, storage cleared before each run. Calls made at boot: identify('user-1', traits), page('Members'), track('Custom view created', …).

    Library Regime At boot After Analytics is granted
    v0.0.14 US_DNSS, Auto library page view, then CDP's identify, page Members, track, all with the consent stamp n/a
    v0.0.14 GDPR, no choice nothing library page view (anonymous); CDP's boot calls are not replayed; new calls go out with the user ID
    v0.0.13 US_DNSS the same four events, no consent stamp n/a
    v0.0.13 GDPR, no choice the same four events, including identify with traits no change
  • Sign-out with the same setup, v0.0.13 and v0.0.14: after identify, calling analytics.reset() without awaiting it and then redirecting (what Auth0Service.logout() does) leaves no ajs_user_id or ajs_user_traits in localStorage or cookies on the next page. Without the call both survive the redirect.

  • Browser check of the production build against latest/ as it was on 2026-11-12 (v0.0.14, wrapper 1.0.7), in headless Chrome, with a test write key. airgap.js was replaced by a fake that implements ready, getConsent, getRegimes and addEventListener. Requests to cdn.segment.com and api.segment.io were intercepted:

    Scenario Requests to Segment Events
    airgap.js blocked, 12 s 0 none
    GDPR, no choice yet 0 none
    …then Analytics granted on the page settings fetched for the CDP key page view with categoryPreferences all true
    GDPR, Analytics refused (Advertising granted) 0 none
    US_DNSS, Auto loads at once page view with consent stamp
    US_DNSS, then Advertising withdrawn 1 Segment Consent Preference Updated, Advertising: false
    US_DNSS, then Analytics withdrawn 0 after withdrawal none

    In every case the script order was airgap.js, then the LFX library.

Not done here: the Segment Source Debugger check on cm.lfx.dev (acceptance item 3). It needs this change deployed to production (staging has no Segment key), and frontend deploys are manual (LF Oracle Production Deploy with frontend). The Transcend allow-list for cm.lfx.dev and lf-staging.crowd.dev is already merged (linuxfoundation/lfx-transcend-config#10).

Decisions

  • Deploy timing: deploy once approved; no need to wait for 2026-11-23. Since 2026-11-13, latest/ serves v0.0.13 again, which has no consent gating, so until the consent release (planned for 2026-11-23) cm.lfx.dev loads Segment for every visitor, in every regime, as the stub does today, plus the library's page view and lfajs_aid. Gating then starts when latest/ moves, without a CDP deploy.
  • Write key: keep CDP's own Segment source (VUE_APP_SEGMENT_KEY).
  • Extra page view and lfajs_aid: accepted. Reports that count page views can drop the library's one with name IS NULL.
  • Sign-out: analytics.reset() added in this PR.
  • Identify after a later grant: accepted here; fixed in the library by linuxfoundation/segment-web-scripts#73 (see Behaviour changes).
  • latest/ vs a pinned version: keep latest/, as every LFX property does. Promotions need prod-latest approval, rollback is the Promote Latest workflow, and a broken library is now logged.

Frontend checklist

Replace the custom Segment snippet stub, which injected analytics.js from
cdn.segment.com with no consent check, with the LFX Segments library from
its latest/ CDN URL. The library waits for Transcend airgap.js, never loads
Segment without Analytics consent, holds opt-in regimes until a confirmed
choice, starts on a later grant, and stamps context.consent on every event.

Call sites now go through a typed analytics module instead of
window.analytics, and the frontend gets a vitest setup for the loader.

Closes linuxfoundation/lfx-self-serve-ops#1148

Signed-off-by: Luis Mori Guerra <[email protected]>
Copilot AI balanced review requested due to automatic review settings November 13, 2026 00:13
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment thread frontend/src/utils/segment.test.ts Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Consent enforcement relies on a mutable external library whose source was unavailable, while deployed consent/withdrawal behavior and Segment source routing still require human validation.

No open issues 🎉

1 file not reviewed
  • pnpm-lock.yaml: Generated file
What changed in this PR

Replaces CDP’s direct Segment loader with the shared LFX analytics library so tracking follows Transcend consent, building on the consent-script integration from #4969.

Changes:

  • Loads the consent-aware LFX library with CDP’s existing write key and forwards analytics calls through a typed module.
  • Migrates existing tracking, page-view, and identification calls away from window.analytics.
  • Adds loader unit tests, Vitest configuration, and frontend CI test execution.
File Description
pnpm-lock.yaml Records frontend Vitest dependency resolution.
frontend/​vitest.config.ts Configures frontend unit-test discovery.
frontend/​src/​utils/​segment.ts Replaces the Segment stub with the LFX loader and typed analytics methods.
frontend/​src/​utils/​segment.test.ts Tests loading, call forwarding, failure handling, and Transcend script order.
frontend/​src/​shared/​modules/​saved-views/​components/​SavedViewManagement.vue Routes saved-view management events through the analytics module.
frontend/​src/​shared/​modules/​saved-views/​components/​forms/​SavedViewForm.vue Migrates saved-view creation and update tracking.
frontend/​src/​shared/​modules/​monitoring/​identify.ts Routes user identification through the analytics module.
frontend/​src/​modules/​organization/​pages/​organization-list-page.vue Migrates organization page-view tracking.
frontend/​src/​modules/​member/​pages/​member-list-page.vue Migrates member page-view tracking and removes an untyped cast.
frontend/​src/​modules/​member/​components/​list/​columns/​member-list-emails.vue Migrates email-contact click tracking.
frontend/​src/​modules/​layout/​pages/​temporary-paywall-page.vue Migrates upgrade-contact tracking.
frontend/​src/​modules/​activity/​pages/​activity-list-page.vue Migrates activity page-view tracking.
frontend/​src/​main.ts Removes stub installation while preserving the write-key check.
frontend/​src/​config/​integrations/​stackoverflow/​components/​stackoverflow-settings-drawer.vue Migrates Stack Overflow drawer tracking.
frontend/​src/​config/​integrations/​reddit/​components/​reddit-settings-drawer.vue Migrates Reddit drawer tracking and removes untyped casts.
frontend/​src/​config/​integrations/​linkedin/​components/​linkedin-settings-drawer.vue Migrates LinkedIn drawer tracking and removes untyped casts.
frontend/​package.json Adds Vitest and a unit-test script.
frontend/​eslint.config.mjs Allows development dependencies in tests and Vitest configuration.
.github/​workflows/​frontend-checks.yml Runs frontend unit tests in CI.

Review effort: Balanced

- withAnalytics logs a failed track/page/identify instead of leaving an
  unhandled rejection: no call site awaits or catches these calls, and a
  method renamed on latest/ would otherwise reach Datadog RUM as an error.
- The fake library records properties and traits, so a dropped payload
  fails the forwarding test.
- index.html check matches <script> tags and type=module case-insensitively
  (CodeQL js/bad-tag-filter).
- Frontend docs: Segment needs Transcend Analytics consent, the test script
  and CI unit-test step, VUE_APP_SEGMENT_KEY is read in config.ts.

Signed-off-by: Luis Mori Guerra <[email protected]>
Copilot AI balanced review requested due to automatic review settings November 13, 2026 15:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Granting or re-granting analytics consent does not restore the signed-in user’s identity, leaving subsequent permitted events anonymous until the user is fetched again.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Re-identify signed-in users when Analytics consent is granted

frontend/​src/​utils/​segment.ts:61

If authentication finishes before Analytics consent is granted, the library drops the identification call, and auth.store.ts:73-76 does not identify again until the user is fetched again. Later permitted events therefore remain anonymous; withdrawing and re-granting consent also loses identification because withdrawal clears the stored identity. Re-identify from the current signed-in auth state when consent is granted and the SDK is ready, without replaying pre-consent tracking events.

1 file not reviewed
  • pnpm-lock.yaml: Generated file

Review effort: Balanced

- loadLfxAnalytics logs when latest/ loads but exposes no instance, so a
  renamed global no longer turns Segment off silently. A blocked script
  stays silent.
- vitest.config.ts resolves the @ alias, and tsconfig type-checks it.
- README: Segment loads when Transcend allows Analytics (US_DNSS is
  opt-out), not only after an explicit choice.
- Tests drop comments that restate the code.

Signed-off-by: Luis Mori Guerra <[email protected]>
Copilot AI balanced review requested due to automatic review settings November 13, 2026 15:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Consent enforcement relies on an inaccessible, mutable external library, and mocked tests leave deployed-host consent gating, withdrawal, and Segment Source Debugger checks requiring human validation.

No open issues 🎉

1 file not reviewed
  • pnpm-lock.yaml: Generated file

Review effort: Balanced

Auth0Service.logout(), which every sign-out path reaches (the user's
logout, a failed /me at boot, and a 401), now calls analytics.reset()
before the Auth0 redirect. The library clears the stored Segment user
ID and traits and its cross-domain link IDs, so the next person on a
shared browser isn't attributed to the previous user.

reset() is not awaited, so sign-out never waits on analytics. Checked in
headless Chrome with v0.0.13 and v0.0.14: after identify, sign-out and
the redirect, ajs_user_id and ajs_user_traits are gone from localStorage
and cookies; without the call both survive.

Signed-off-by: Luis Mori Guerra <[email protected]>
Copilot AI balanced review requested due to automatic review settings November 13, 2026 16:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human assessment is needed to evaluate the critical risk that the mutable analytics artifact sends events and identity traits without consent.

No open issues 🎉

1 file not reviewed
  • pnpm-lock.yaml: Generated file

Review effort: Balanced

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants