Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { LinkifyPipe } from '@pipes/linkify.pipe';
import { MeetingTimePipe } from '@pipes/meeting-time.pipe';
import { RecurrenceSummaryPipe } from '@pipes/recurrence-summary.pipe';
import { MeetingService } from '@services/meeting.service';
import { PlausibleService } from '@services/plausible.service';
import { ProjectContextService } from '@services/project-context.service';
import { ProjectService } from '@services/project.service';
import { UserService } from '@services/user.service';
Expand Down Expand Up @@ -115,6 +116,7 @@ export class MeetingJoinComponent implements OnInit {
private readonly userService = inject(UserService);
private readonly clipboard = inject(Clipboard);
private readonly projectContextService = inject(ProjectContextService);
private readonly plausibleService = inject(PlausibleService);
private readonly dialogService = inject(DialogService);
private readonly destroyRef = inject(DestroyRef);
private readonly platformId = inject(PLATFORM_ID);
Expand Down Expand Up @@ -297,6 +299,7 @@ export class MeetingJoinComponent implements OnInit {
this.registrants = this.initializeRegistrants();
this.parentProject = this.initializeParentProject();
this.initializeAutoJoin();
this.initializePublicMeetingPageviewTracking();
}

public ngOnInit(): void {
Expand Down Expand Up @@ -1128,6 +1131,46 @@ export class MeetingJoinComponent implements OnInit {
);
}

// PlausibleService defers the auto-pageview for /meetings/:id — fire it here once context lands.
private initializePublicMeetingPageviewTracking(): void {
if (isPlatformServer(this.platformId)) {
return;
}
// Caps the parent foundation fetch so ROOT_PROJECT_SLUG → null (mapped in initializeParentProject)
// doesn't block the pageview forever. Sub-projects whose parent doesn't land in time record
// project_* correctly and leave foundation_* empty (see buildPageviewContext callsite below).
const PARENT_FETCH_TIMEOUT_MS = 2000;
toObservable(this.project)
.pipe(
filter((p): p is Partial<Project> => !!p?.slug),
switchMap((project) => {
if (!project.parent_uid) {
return of({ project, parent: null as Project | null });
}
return merge(
toObservable(this.parentProject).pipe(
filter((parent): parent is Project => !!parent),
Comment thread
niravpatel27 marked this conversation as resolved.
map((parent) => ({ project, parent: parent as Project | null }))
),
timer(PARENT_FETCH_TIMEOUT_MS).pipe(map(() => ({ project, parent: null as Project | null })))
);
Comment thread
niravpatel27 marked this conversation as resolved.
}),
take(1),
takeUntilDestroyed(this.destroyRef)
Comment thread
niravpatel27 marked this conversation as resolved.
)
.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,
})
);
});
Comment thread
niravpatel27 marked this conversation as resolved.
Comment on lines +1158 to +1171
}

private initializeRegistrants(): Signal<MeetingRegistrant[]> {
return toSignal(
combineLatest([
Expand Down
71 changes: 58 additions & 13 deletions apps/lfx-one/src/app/shared/services/plausible.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router } from '@angular/router';
import { environment } from '@environments/environment';
import { PLAUSIBLE_DOMAIN, PLAUSIBLE_SRC } from '@lfx-one/shared/constants';
import { PlausibleCall } from '@lfx-one/shared/interfaces';
import { PlausibleCall, PlausiblePageviewContext } from '@lfx-one/shared/interfaces';
import { filter } from 'rxjs';

import { LensService } from './lens.service';
import { ProjectContextService } from './project-context.service';

/**
* Plausible analytics service for privacy-friendly page and event tracking.
* Uses Angular's afterNextRender for SSR-safe script loading.
Expand All @@ -19,6 +22,11 @@ import { filter } from 'rxjs';
export class PlausibleService {
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
private readonly projectContextService = inject(ProjectContextService);
private readonly lensService = inject(LensService);

// Routes whose owning component fires `trackPage()` itself once async context resolves.
private static readonly deferredPageviewPattern = /^\/meetings\/\d+(-\d{13})?$/;

private scriptLoaded = false;
private analyticsReady = false;
Expand Down Expand Up @@ -47,22 +55,43 @@ export class PlausibleService {
});
}

/**
* Track a page view
* @param properties Optional page properties
*/
public trackPage(properties?: Record<string, unknown>): void {
// Auto-prepends sanitized path/url/title — callers only supply context.
public trackPage(context?: PlausiblePageviewContext): void {
if (typeof window === 'undefined' || this.impersonating || !this.analyticsReady || !window.plausible) {
return;
}

try {
window.plausible('pageview', { u: this.getSanitizedUrl(), props: properties });
const url = this.getSanitizedUrl();
const props: Record<string, unknown> = {
path: this.getSanitizedPath(window.location.pathname),
url,
title: typeof document !== 'undefined' ? document.title : undefined,
...(context as Record<string, unknown> | undefined),
};
window.plausible('pageview', { u: url, props });
} catch (error) {
console.error('Error tracking page with Plausible:', error);
}
}

// Single owner of the pageview custom-prop schema — keep new dimensions here, not at callsites.
public static buildPageviewContext(input: {
foundationSlug?: string | null;
foundationName?: string | null;
projectSlug?: string | null;
projectName?: string | null;
lens?: string | null;
}): PlausiblePageviewContext {
const context: PlausiblePageviewContext = {};
if (input.foundationSlug) context.foundation = input.foundationSlug;
if (input.foundationName) context.foundation_name = input.foundationName;
if (input.projectSlug) context.project = input.projectSlug;
if (input.projectName) context.project_name = input.projectName;
if (input.lens) context.lens = input.lens;
return context;
}

/**
* Track a custom event
* @param eventName Event name
Expand Down Expand Up @@ -117,7 +146,10 @@ export class PlausibleService {
// only after the bundle has executed and replaced the queue stub.
script.onload = () => {
this.analyticsReady = true;
this.trackPage();
if (PlausibleService.deferredPageviewPattern.test(window.location.pathname)) {
return;
}
this.trackPage(this.buildContextProps());
Comment on lines +149 to +152
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.

};

script.onerror = (error) => {
Expand Down Expand Up @@ -149,14 +181,27 @@ export class PlausibleService {
if (typeof document === 'undefined') {
return;
}
this.trackPage({
path: this.getSanitizedPath(event.urlAfterRedirects),
url: this.getSanitizedUrl(),
title: document.title,
});
const path = this.getSanitizedPath(event.urlAfterRedirects);
if (PlausibleService.deferredPageviewPattern.test(path)) {
return;
}
this.trackPage(this.buildContextProps());
});
}

// Foundation/project read independently (not via activeContext) so the project lens emits both.
private buildContextProps(): PlausiblePageviewContext {
const foundation = this.projectContextService.selectedFoundation();
const project = this.projectContextService.selectedProject();
return PlausibleService.buildPageviewContext({
foundationSlug: foundation?.slug,
foundationName: foundation?.name,
projectSlug: project?.slug,
projectName: project?.name,
lens: this.lensService.activeLens(),
});
}

/**
* Build a privacy-safe URL string for Plausible.
* Strips query params and hash to avoid leaking auth tokens, OTPs, or
Expand Down
9 changes: 9 additions & 0 deletions packages/shared/src/interfaces/plausible.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ export interface PlausibleConfig {
enabled: boolean;
}

// Custom-prop schema attached to Plausible pageviews — keys omitted when unknown.
export interface PlausiblePageviewContext {
foundation?: string;
foundation_name?: string;
project?: string;
project_name?: string;
lens?: string;
}

/**
* One queued Plausible call captured by the upstream snippet's queue stub
* before the real script loads. Each entry is the full argument tuple the
Expand Down
Loading