feat(analytics): tag Plausible pageviews with foundation/project/lens#709
Conversation
PlausibleService now reads ProjectContextService and LensService on every NavigationEnd, attaching foundation / foundation_name / project / project_name / lens custom-props to each pageview so the dashboard becomes filterable and groupable by those dimensions. Previously opaque entries like /meetings/<id> can now be sliced by their owning foundation/project. Public /meetings/:numericId routes load project context asynchronously, so the auto NavigationEnd pageview is deferred for those paths and MeetingJoinComponent fires one enriched pageview once meeting + project (and parent foundation when applicable) have resolved. A 2s timeout race ensures the event still fires when the parent maps to the root project and is returned as null by initializeParentProject. No route, redirect, guard, or user-visible behavior changes. Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
WalkthroughAdds enriched Plausible pageview tracking: a new PlausiblePageviewContext interface, PlausibleService now builds/emits context and defers meeting route pageviews, and MeetingJoinComponent fires one client-only pageview after project slug (with a 2s parent-resolution race for sub-projects). ChangesMeeting pageview enrichment
Sequence Diagram(s)sequenceDiagram
participant MeetingJoinComponent
participant PlausibleService
participant ProjectContextService
participant ParentProjectLookup
participant Plausible
MeetingJoinComponent->>PlausibleService: initializePublicMeetingPageviewTracking()
PlausibleService->>ProjectContextService: selectedProject() (wait for slug)
alt project has parent_uid
PlausibleService->>ParentProjectLookup: fetch parentProject() (race with 2s timer)
alt parent resolves within 2s
PlausibleService->>Plausible: trackPage(buildMeetingPageviewProps(project,parent))
else timeout or unresolved
PlausibleService->>Plausible: trackPage(buildMeetingPageviewProps(project,null))
end
else top-level project
PlausibleService->>Plausible: trackPage(buildMeetingPageviewProps(project,null))
end
🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Pull request overview
Adds Plausible custom-property tagging (foundation, foundation_name, project, project_name, lens) to pageviews so the analytics dashboard can be sliced by those dimensions. For public /meetings/:id routes (where context resolves async after navigation), the auto NavigationEnd pageview is suppressed and MeetingJoinComponent fires one enriched pageview after the project (and parent foundation, if any) resolves, with a 2s timeout fallback.
Changes:
- New
PlausiblePageviewContextshared interface (5 optional string keys). PlausibleServicenow readsProjectContextService/LensService, merges context props into auto pageviews, and skips the auto-pageview for/meetings/<digits>paths.MeetingJoinComponentfires a single deferred enriched pageview onceproject()(and parent) resolve, racing a 2s timer for theparent_uid → ROOT_PROJECT_SLUG → nullcase.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
packages/shared/src/interfaces/plausible.interface.ts |
Adds PlausiblePageviewContext interface. |
apps/lfx-one/src/app/shared/services/plausible.service.ts |
Builds and merges foundation/project/lens props; defers initial + route pageviews on /meetings/:id. |
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts |
Fires one deferred enriched Plausible pageview once meeting context resolves, with a 2s parent-fetch timeout race. |
Comments suppressed due to low confidence (1)
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts:1162
parentProjectis built viatoSignal(..., { initialValue: null }), so its underlying observable's first emitted value (the synchronous initialnull) is filtered out byfilter(parent => !!parent). However, whenparent_uidis set and the lookup legitimately resolves tonull(parent maps toROOT_PROJECT_SLUG, or the API errors out viacatchError(() => of(null))), that resolvednullis also filtered out — making the pipeline indistinguishable from "still loading". This is what forces the 2s timer fallback. Emitting a discriminator (e.g., resolving anOptional<Project>or{ resolved: true, parent }) frominitializeParentProjectwould let this pipeline await the actual resolution without the arbitrary timeout.
toObservable(this.parentProject).pipe(
filter((parent): parent is Project => !!parent),
map((parent) => ({ project, parent: parent as Project | null }))
),
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts`:
- Around line 1143-1171: The manual pageview in
initializePublicMeetingPageviewTracking currently fires for all meeting pages
and can double-count when automatic suppression only targets routes matching
^/meetings/\d+$; update the subscription to only call
this.plausibleService.trackPage(this.buildMeetingPageviewProps(project, parent))
when the current route matches the same regex (i.e., the deferred-route form
used for auto-suppression). Locate initializePublicMeetingPageviewTracking and
add a guard using the component's routing/location state (e.g., this.router.url
or this.location.path()) to test against /^\/meetings\/\d+$/ (or the same
matcher used elsewhere) before invoking plausibleService.trackPage so manual
tracking scope aligns with deferred routes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 91ad9e8b-3682-4f73-980b-60fcfe5be352
📒 Files selected for processing (3)
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.tsapps/lfx-one/src/app/shared/services/plausible.service.tspackages/shared/src/interfaces/plausible.interface.ts
Address two issues raised in PR review: - Extend deferredPageviewPattern to also match past-meeting occurrence URLs of the form /meetings/<id>-<13-digit-timestamp> (matches the shape that isPastMeetingOccurrenceId validates). Previously these paths slipped past the auto-pageview skip and produced a context-free NavigationEnd event in addition to the deferred enriched event from MeetingJoinComponent — double-counting every past-meeting visit. - Branch buildMeetingPageviewProps on project.parent_uid rather than on whether parent resolved. When parent_uid is set but the lookup is slow, the 2s timer race used to fire with parent=null, which mis-attributed the sub-project under foundation_* and dropped project_* entirely. The new branch records the sub-project correctly under project_* and leaves foundation_* empty when unresolved — honest gap vs. data corruption in the dashboard dimensions this PR is introducing. Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
MRashad26
left a comment
There was a problem hiding this comment.
Strict CLAUDE.md / code-enforcer-agent review — 2 major + 3 minor findings. Skipping items already raised by Copilot (L1164: 2s timer races indistinguishably with legit null parent resolution) and CodeRabbit (L1171: auto-suppression ^/meetings/\d+$ vs manual fire on all meeting paths could double-count).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts`:
- Around line 1147-1167: The subscription currently uses take(1) which completes
after the first meeting and prevents further per-route pageview tracking; remove
the take(1) and instead ensure you only track when the meeting identity changes
by adding a distinct-until check on the project slug (e.g., use
distinctUntilChanged comparing project.slug) before emitting the pageview in the
pipeline that starts with toObservable(this.project) / switchMap(...) so the
handler continues to run for in-component route changes and still completes on
component destroy via takeUntilDestroyed(this.destroyRef).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: fcc0248f-0995-4ec8-b1fa-096adc7f1d26
📒 Files selected for processing (2)
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.tsapps/lfx-one/src/app/shared/services/plausible.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/lfx-one/src/app/shared/services/plausible.service.ts
Address review feedback from MRashad26:
- Extract PlausibleService.buildPageviewContext({...}) as the single owner
of the pageview custom-prop schema. Both buildContextProps (service) and
the deferred-pageview callsite in MeetingJoinComponent route through it,
so adding a new dimension never lands in just one of the two callsites.
- trackPage(context?: PlausiblePageviewContext) now auto-prepends
path/url/title via getSanitizedPath/getSanitizedUrl. Callers only supply
context — MeetingJoinComponent no longer touches window.location or
document.title directly, removing the privacy/sanitization-bypass risk
if a future public meeting URL ever carried query-string tokens.
- Drop redundant `return { ...context };` spread on buildContextProps —
the new typed return is PlausiblePageviewContext.
- Name the parent-fetch timeout (PARENT_FETCH_TIMEOUT_MS = 2000) so its
load-bearing role isn't buried as a bare literal.
- Strip multi-paragraph JSDocs and multi-line inline comments added in
this PR per CLAUDE.md ("default to writing no comments. Never write
multi-paragraph docstrings or multi-line comment blocks").
No behavior or data change: same pageview frequency, same custom-prop
values populated under the same conditions. Initial script.onload
pageview now also includes path/url/title in props (previously only via
`u`); strictly additive.
Signed-off-by: Nirav Patel <npatel@linuxfoundation.org>
| take(1), | ||
| takeUntilDestroyed(this.destroyRef) | ||
| ) | ||
| .subscribe(({ project, parent }) => { | ||
| const isTopLevel = !project.parent_uid; | ||
| this.plausibleService.trackPage( | ||
| PlausibleService.buildPageviewContext({ | ||
| foundationSlug: isTopLevel ? project.slug : parent?.slug, | ||
| foundationName: isTopLevel ? project.name : parent?.name, | ||
| projectSlug: isTopLevel ? undefined : project.slug, | ||
| projectName: isTopLevel ? undefined : project.name, | ||
| }) | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts (1)
1143-1159:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep this tracker alive for later
/meetings/...navigations.
initializeMeeting()updatesthis.projectfromparamMap, buttake(1)completes this stream after the first meeting. BecausePlausibleServiceskips automatic pageviews for these routes, navigating within the reusedMeetingJoinComponentto another meeting or past-occurrence URL will never emit a second pageview. Dedupe on the current meeting/route instead of completing the subscription here.In Angular Router, when navigating between URLs handled by the same route definition but with different route parameters, does Angular reuse the existing component instance by default?🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts` around lines 1143 - 1159, The stream currently ends prematurely because of take(1) on toObservable(this.project); remove take(1) and instead dedupe emissions by the meeting/route so the tracker stays alive across navigations. Replace the take(1) with an operator like distinctUntilChanged (or distinctUntilKeyChanged) that compares a stable key (e.g., project.slug or the meeting/route-identifying params) so initializeMeeting() can update this.project and only distinct meeting/route changes emit; keep takeUntilDestroyed(this.destroyRef) to clean up. Ensure the logic inside switchMap (parent lookup + timeout) still runs for each new distinct project key.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/lfx-one/src/app/shared/services/plausible.service.ts`:
- Around line 149-152: The early return that checks
PlausibleService.deferredPageviewPattern and then skips
this.trackPage(this.buildContextProps()) causes lost pageviews when
MeetingJoinComponent resolves context before the Plausible script is ready;
change the logic so you do not suppress the onload fallback until the deferred
hit is actually queued or analytics is ready — either (A) call
this.trackPage(...) unconditionally but modify trackPage to enqueue hits when
the real Plausible stub isn’t ready (so deferred hits are stored and flushed
later), or (B) guard the deferred-page suppression by checking the
analytics-ready flag (e.g., PlausibleService.isReady or similar) and only return
if the service is ready and the pattern matches; update references in
PlausibleService (deferredPageviewPattern, trackPage, buildContextProps) and in
MeetingJoinComponent usage accordingly.
---
Duplicate comments:
In
`@apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts`:
- Around line 1143-1159: The stream currently ends prematurely because of
take(1) on toObservable(this.project); remove take(1) and instead dedupe
emissions by the meeting/route so the tracker stays alive across navigations.
Replace the take(1) with an operator like distinctUntilChanged (or
distinctUntilKeyChanged) that compares a stable key (e.g., project.slug or the
meeting/route-identifying params) so initializeMeeting() can update this.project
and only distinct meeting/route changes emit; keep
takeUntilDestroyed(this.destroyRef) to clean up. Ensure the logic inside
switchMap (parent lookup + timeout) still runs for each new distinct project
key.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 1aac77c2-d040-439b-917c-ee93cb12f577
📒 Files selected for processing (3)
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.tsapps/lfx-one/src/app/shared/services/plausible.service.tspackages/shared/src/interfaces/plausible.interface.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/shared/src/interfaces/plausible.interface.ts
| if (PlausibleService.deferredPageviewPattern.test(window.location.pathname)) { | ||
| return; | ||
| } | ||
| this.trackPage(this.buildContextProps()); |
There was a problem hiding this comment.
Don't suppress the onload fallback before the deferred hit can actually be sent.
MeetingJoinComponent can resolve its project context before the Plausible script finishes loading. trackPage() drops pre-ready calls, so this early return removes the only fallback for /meetings/... and the initial pageview is lost on fast API / slow CDN loads. Either queue the deferred hit against the stub or wait for analytics readiness before suppressing the onload send.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/lfx-one/src/app/shared/services/plausible.service.ts` around lines 149 -
152, The early return that checks PlausibleService.deferredPageviewPattern and
then skips this.trackPage(this.buildContextProps()) causes lost pageviews when
MeetingJoinComponent resolves context before the Plausible script is ready;
change the logic so you do not suppress the onload fallback until the deferred
hit is actually queued or analytics is ready — either (A) call
this.trackPage(...) unconditionally but modify trackPage to enqueue hits when
the real Plausible stub isn’t ready (so deferred hits are stored and flushed
later), or (B) guard the deferred-page suppression by checking the
analytics-ready flag (e.g., PlausibleService.isReady or similar) and only return
if the service is ready and the pattern matches; update references in
PlausibleService (deferredPageviewPattern, trackPage, buildContextProps) and in
MeetingJoinComponent usage accordingly.
Summary
PlausibleServicenow readsProjectContextService+LensServiceon everyNavigationEndand attachesfoundation/foundation_name/project/project_name/lenscustom-props to each pageview, so the Plausible dashboard becomes filterable/groupable by these dimensions./meetings/:numericIdroutes load project context asynchronously — the auto NavigationEnd pageview is deferred for those paths andMeetingJoinComponentfires one enriched pageview once the meeting + project (and parent foundation when applicable) have resolved. A 2s timeout race guarantees the event still fires when the parent maps toROOT_PROJECT_SLUGand is therefore returned asnull.PlausiblePageviewContextin@lfx-one/shared/interfaces/plausible.interface.ts.No route, redirect, guard, or user-visible behavior changes. Total ≈ 142 lines across 3 files.
Files changed
packages/shared/src/interfaces/plausible.interface.tsPlausiblePageviewContextinterface (5 optional string keys)apps/lfx-one/src/app/shared/services/plausible.service.tsProjectContextService+LensService; newbuildContextProps(); merge into NavigationEnd + initialscript.onloadpageviews; skip auto-pageview for/meetings/<digits>pathsapps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.tsPlausibleService; newinitializePublicMeetingPageviewTracking()fires deferred enriched pageview after project resolves (browser-only, single-shot, with parent-fetch timeout race)Why
The Plausible dashboard at
app.lfx.devshows entry pages like/meetings/92079944361with no signal of which foundation/project the visit belongs to. With this change the dashboard's existing Properties panel surfacesfoundation/project/lensas filterable dimensions — operators can finally answer "how much traffic does the Kubernetes foundation see?" or slice top-pages by project. URLs are intentionally NOT normalized (e.g./meetings/:id) — kept as-is so existing saved views and per-meeting drilldown still work.5 custom-prop keys, well under Plausible's 30-keys-per-site limit. Historical pageviews predating the deploy will show up under
(none)when grouping by these props.Test plan
environment.ts→plausible.enabled: true, sign in, navigate/foundation/meetings?project=<slug>→ confirm POST tohttps://plausible.io/api/eventincludesprops.foundation/props.foundation_name/props.project/props.project_name/props.lensmatching the active context/meetings/<real-numeric-id>→ confirm (a) NO Plausible POST on initialNavigationEnd, (b) exactly ONE POST after the/public/api/meetings/:idresponse settles, (c) that POST carries foundation/project props from the API responseproject: nullpayload does NOT fire any Plausible POSTsetImpersonating(true)short-circuit still suppresses the enriched pageviewfoundation,foundation_name,project,project_name,lensas filterable dimensionsNotes
gpg.ssh.allowedSignersFilenot set), so the commit may show as unverified on GitHub — happy to re-sign if needed.LFXV2-<n>after triage.