Skip to content

feat(analytics): tag Plausible pageviews with foundation/project/lens#709

Merged
niravpatel27 merged 3 commits into
mainfrom
feat/plausible-context-enrichment
May 14, 2026
Merged

feat(analytics): tag Plausible pageviews with foundation/project/lens#709
niravpatel27 merged 3 commits into
mainfrom
feat/plausible-context-enrichment

Conversation

@niravpatel27
Copy link
Copy Markdown
Contributor

Summary

  • PlausibleService now reads ProjectContextService + LensService on every NavigationEnd and attaches foundation / foundation_name / project / project_name / lens custom-props to each pageview, so the Plausible dashboard becomes filterable/groupable by these dimensions.
  • Public /meetings/:numericId routes load project context asynchronously — the auto NavigationEnd pageview is deferred for those paths and MeetingJoinComponent fires 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 to ROOT_PROJECT_SLUG and is therefore returned as null.
  • New shared interface PlausiblePageviewContext in @lfx-one/shared/interfaces/plausible.interface.ts.

No route, redirect, guard, or user-visible behavior changes. Total ≈ 142 lines across 3 files.

Files changed

File Change
packages/shared/src/interfaces/plausible.interface.ts Add PlausiblePageviewContext interface (5 optional string keys)
apps/lfx-one/src/app/shared/services/plausible.service.ts Inject ProjectContextService + LensService; new buildContextProps(); merge into NavigationEnd + initial script.onload pageviews; skip auto-pageview for /meetings/<digits> paths
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts Inject PlausibleService; new initializePublicMeetingPageviewTracking() fires deferred enriched pageview after project resolves (browser-only, single-shot, with parent-fetch timeout race)

Why

The Plausible dashboard at app.lfx.dev shows entry pages like /meetings/92079944361 with no signal of which foundation/project the visit belongs to. With this change the dashboard's existing Properties panel surfaces foundation / project / lens as 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

  • Local smoke: temporarily flip environment.tsplausible.enabled: true, sign in, navigate /foundation/meetings?project=<slug> → confirm POST to https://plausible.io/api/event includes props.foundation / props.foundation_name / props.project / props.project_name / props.lens matching the active context
  • Public meeting (incognito): open /meetings/<real-numeric-id> → confirm (a) NO Plausible POST on initial NavigationEnd, (b) exactly ONE POST after the /public/api/meetings/:id response settles, (c) that POST carries foundation/project props from the API response
  • Anonymous on private meeting: confirm redacted project: null payload does NOT fire any Plausible POST
  • Impersonation: trigger impersonation → confirm setImpersonating(true) short-circuit still suppresses the enriched pageview
  • Post-deploy: Plausible dashboard → Properties panel surfaces foundation, foundation_name, project, project_name, lens as filterable dimensions

Notes

  • DCO sign-off attached. Local GPG/SSH signing config is incomplete on this machine (gpg.ssh.allowedSignersFile not set), so the commit may show as unverified on GitHub — happy to re-sign if needed.
  • JIRA ticket not linked yet — please attach LFXV2-<n> after triage.

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>
@niravpatel27 niravpatel27 requested a review from a team as a code owner May 14, 2026 15:55
Copilot AI review requested due to automatic review settings May 14, 2026 15:55
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Review Change Stack

Walkthrough

Adds 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).

Changes

Meeting pageview enrichment

Layer / File(s) Summary
Plausible pageview context contract
packages/shared/src/interfaces/plausible.interface.ts
New PlausiblePageviewContext interface exports optional fields (foundation, foundation_name, project, project_name, lens) for enriching Plausible pageview events.
PlausibleService enrichment and deferred tracking
apps/lfx-one/src/app/shared/services/plausible.service.ts
PlausibleService injects ProjectContextService and LensService, adds deferredPageviewPattern to identify meeting URLs, changes trackPage to accept PlausiblePageviewContext, implements buildPageviewContext() and buildContextProps(), and updates script onload and route NavigationEnd handling to suppress or send enriched pageviews.
MeetingJoinComponent single-shot pageview tracking
apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts
Component imports/injects PlausibleService, calls initializePublicMeetingPageviewTracking() from the constructor, waits for project.slug, races parentProject resolution against a 2s timer for sub-projects, and emits exactly one trackPage(...) with conditional foundation/project fields.

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
Loading

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • linuxfoundation/lfx-self-serve#701: Updates ProjectContextService routing guards to seed and sync ?project=<slug>, which affects foundation/project context consumed by PlausibleService.

Suggested labels

deploy-preview

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main change: adding foundation/project/lens context tags to Plausible pageviews, which is the core objective across all three modified files.
Description check ✅ Passed The description is directly related to the changeset, providing context, rationale, test plan, and file-by-file breakdown of the analytics enrichment changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plausible-context-enrichment

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 PlausiblePageviewContext shared interface (5 optional string keys).
  • PlausibleService now reads ProjectContextService/LensService, merges context props into auto pageviews, and skips the auto-pageview for /meetings/<digits> paths.
  • MeetingJoinComponent fires a single deferred enriched pageview once project() (and parent) resolve, racing a 2s timer for the parent_uid → ROOT_PROJECT_SLUG → null case.

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

  • parentProject is built via toSignal(..., { initialValue: null }), so its underlying observable's first emitted value (the synchronous initial null) is filtered out by filter(parent => !!parent). However, when parent_uid is set and the lookup legitimately resolves to null (parent maps to ROOT_PROJECT_SLUG, or the API errors out via catchError(() => of(null))), that resolved null is also filtered out — making the pipeline indistinguishable from "still loading". This is what forces the 2s timer fallback. Emitting a discriminator (e.g., resolving an Optional<Project> or { resolved: true, parent }) from initializeParentProject would 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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between a3d54e9 and d9ba4ba.

📒 Files selected for processing (3)
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts
  • apps/lfx-one/src/app/shared/services/plausible.service.ts
  • packages/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>
Copy link
Copy Markdown
Contributor

@MRashad26 MRashad26 left a comment

Choose a reason for hiding this comment

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

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).

Comment thread apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts Outdated
Comment thread apps/lfx-one/src/app/shared/services/plausible.service.ts Outdated
Comment thread apps/lfx-one/src/app/shared/services/plausible.service.ts Outdated
Comment thread apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between d9ba4ba and 706ed86.

📒 Files selected for processing (2)
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts
  • apps/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>
Copilot AI review requested due to automatic review settings May 14, 2026 16:18
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment on lines +1158 to +1171
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,
})
);
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 win

Keep this tracker alive for later /meetings/... navigations.

initializeMeeting() updates this.project from paramMap, but take(1) completes this stream after the first meeting. Because PlausibleService skips automatic pageviews for these routes, navigating within the reused MeetingJoinComponent to 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

📥 Commits

Reviewing files that changed from the base of the PR and between 706ed86 and 1537d87.

📒 Files selected for processing (3)
  • apps/lfx-one/src/app/modules/meetings/meeting-join/meeting-join.component.ts
  • apps/lfx-one/src/app/shared/services/plausible.service.ts
  • packages/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

Comment on lines +149 to +152
if (PlausibleService.deferredPageviewPattern.test(window.location.pathname)) {
return;
}
this.trackPage(this.buildContextProps());
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

@niravpatel27 niravpatel27 merged commit c990ccf into main May 14, 2026
16 checks passed
@niravpatel27 niravpatel27 deleted the feat/plausible-context-enrichment branch May 14, 2026 16:26
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.

3 participants