AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Frontend Observability

skill-stareezy-1-frontend-architecture-skill-frontend-observability · by stareezy-1

A portable, framework-agnostic field-side observability system for any React or React Native app. Establishes one typed event taxonomy (canonical event-name constants, never inline strings), a best-effort non-blocking provider fan-out so a failing or absent analytics provider can never throw into the app or block other providers, a single track() entry point exposed through a context hook, real-u…

No reviews yet
0 installs
25 views
0.0% view→install

Install

$ agentstack add skill-stareezy-1-frontend-architecture-skill-frontend-observability

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-stareezy-1-frontend-architecture-skill-frontend-observability)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Frontend Observability? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Frontend Observability (the field side)

> Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others. > This skill describes a field-side observability system — event taxonomy, provider fan-out, > real-user vitals, error reporting, consent — not a dashboard or a specific vendor. It is the > field complement to the frontend-lighthouse skill: Lighthouse is the lab gate (synthetic, > pre-merge); this is the field (what real users actually experience). It lives in a > services/analytics/ module per the frontend-architecture skill.

The goal: you can answer "what are real users doing, and what are they experiencing?" — with a typed event vocabulary (no stringly-typed track("clicked_thing") scattered everywhere), a fan-out that is best-effort (a broken provider never breaks the app), real Core Web Vitals from the field, and consent respected before anything fires.


0. The five core ideas

  1. Events are a typed vocabulary. Event names are canonical constants with a union type — never inline string literals. The taxonomy is reviewable in one file and the compiler rejects typos.
  2. Fan-out is best-effort and non-blocking. track() dispatches to every provider, each in its own try/catch. A missing global, a thrown provider, an unloaded script — none can throw into the caller or stop the other providers.
  3. One entry point, SSR-safe. A single track(event, props) is the only way to record. It's reached through a context hook that no-ops outside a provider and on the server, so instrumented components render safely anywhere.
  4. Field vitals complement lab budgets. Real-user LCP/INP/CLS are reported to the same fan-out. Lighthouse proves the build can be fast; field vitals prove it is — together they close the loop.
  5. Consent gates everything. No telemetry (events, vitals, error reports with PII) fires before opt-in. Consent state is checked at the fan-out boundary, not sprinkled through call sites.

1. Directory layout

The system is one service module plus its constants (per frontend-architecture).

src/
├── constants/
│   └── analytics.ts           ← canonical event names + AnalyticsEvent union
├── services/analytics/
│   ├── index.ts               ← barrel: track, adapters, types
│   ├── track.ts               ← the best-effort fan-out (single entry point)
│   ├── adapters.ts            ← one (event, props) => void per provider, window-guarded
│   ├── web-vitals.ts          ← report real-user LCP/INP/CLS into track()
│   └── consent.ts             ← consent gate read by the fan-out
├── providers/
│   └── AnalyticsProvider.tsx  ← 'use client' context exposing useAnalytics().track
└── error/
    └── ErrorBoundary.tsx      ← reports caught render errors via the fan-out

2. The event taxonomy (typed, never inline)

One file owns every event name. Components reference constants; the union type makes typos a compile error and the catalog a single source of truth.

// constants/analytics.ts
export const ANALYTICS_EVENTS = {
  PROJECT_CLICK: "project_click",
  GITHUB_CLICK: "github_click",
  RESUME_DOWNLOAD: "resume_download",
  CONTACT_SUBMISSION: "contact_submission",
} as const;

export type AnalyticsEvent =
  (typeof ANALYTICS_EVENTS)[keyof typeof ANALYTICS_EVENTS];
// CORRECT — typed constant, autocompletes, can't typo
track(ANALYTICS_EVENTS.GITHUB_CLICK, { url });

// WRONG — stringly-typed, drifts, no compile check
track("github-click"); // ❌ silently a different event from "github_click"

Hard rules:

  • No inline event-name strings anywhere; only ANALYTICS_EVENTS.*.
  • Event names are snake_case and stable — renaming one breaks historical dashboards, so treat the catalog as a contract.
  • Keep props shapes small and PII-light (see §6); prefer ids over names, never raw emails.

3. Best-effort, non-blocking fan-out

track() is the single entry point. It iterates the adapter registry, guarding each call so one provider can't affect the caller or the others.

// services/analytics/track.ts
import type { AnalyticsEvent } from "@/constants/analytics";
import { analyticsAdapters } from "./adapters";
import { hasConsent } from "./consent";

export function track(
  event: AnalyticsEvent,
  props?: Record,
): void {
  if (!hasConsent()) return; // §6 — nothing fires before opt-in
  for (const adapter of analyticsAdapters) {
    try {
      adapter(event, props);
    } catch {
      /* best-effort: a failing/absent provider must never throw into the
         caller or block dispatch to the remaining providers. */
    }
  }
}

Each adapter is a tiny (event, props) => void that guards its provider global — it no-ops on the server (no window) and when the provider script is absent, so a missing or unloaded provider never throws.

// services/analytics/adapters.ts
export type AnalyticsAdapter = (
  event: AnalyticsEvent,
  props?: Record,
) => void;

export const googleAnalyticsAdapter: AnalyticsAdapter = (event, props) => {
  const w =
    typeof window !== "undefined" ? (window as AnalyticsGlobals) : undefined;
  if (!w || typeof w.gtag !== "function") return; // SSR-safe + absent-safe
  w.gtag("event", event, props ?? {});
};

export const clarityAdapter: AnalyticsAdapter = (event) => {
  const w =
    typeof window !== "undefined" ? (window as AnalyticsGlobals) : undefined;
  if (!w || typeof w.clarity !== "function") return;
  w.clarity("event", event);
};

// The registry track() fans out across. Exported + mutable so tests can swap
// in a recording sink to assert dispatch.
export const analyticsAdapters: AnalyticsAdapter[] = [
  googleAnalyticsAdapter,
  clarityAdapter,
  firebaseAdapter,
  // posthogAdapter, openPanelAdapter, …
];

3.1 Firebase Analytics — one adapter, two platforms

Firebase Analytics ships two SDKs that share the same logEvent(name, params) contract, so a single conceptual adapter covers both web and React Native — only the import and the "is it available?" guard differ. On web the adapter never imports the SDK at module top level (it's browser-only and async), so it stays SSR-safe.

// services/analytics/adapters.firebase.web.ts — Firebase JS SDK (web)
import type { Analytics } from "firebase/analytics";
import type { AnalyticsAdapter } from "./adapters";

// Held after a lazy, browser-only init (below) so the adapter stays synchronous + SSR-safe.
let analytics: Analytics | undefined;
export function setFirebaseAnalytics(instance: Analytics): void {
  analytics = instance;
}

export const firebaseAdapter: AnalyticsAdapter = (event, props) => {
  if (typeof window === "undefined" || !analytics) return; // SSR-safe + not-yet-ready safe
  void import("firebase/analytics").then(({ logEvent }) =>
    logEvent(analytics!, event, props),
  );
};
// services/analytics/firebase.init.ts — lazy, browser-only init (web)
import { initializeApp, getApps } from "firebase/app";
import { getAnalytics, isSupported } from "firebase/analytics";
import { setFirebaseAnalytics } from "./adapters.firebase.web";
import { FIREBASE_CONFIG } from "@/constants/analytics";

export async function initFirebaseAnalytics(): Promise {
  if (typeof window === "undefined") return; // never on the server
  if (!(await isSupported())) return; // unsupported browser → no-op
  const app = getApps()[0] ?? initializeApp(FIREBASE_CONFIG);
  setFirebaseAnalytics(getAnalytics(app)); // adapter goes live after this
}
// services/analytics/adapters.firebase.native.ts — @react-native-firebase/analytics (RN / Expo)
import analytics from "@react-native-firebase/analytics";
import type { AnalyticsAdapter } from "./adapters";

export const firebaseAdapter: AnalyticsAdapter = (event, props) => {
  // RN: no window; the native module is present once the app boots.
  void analytics().logEvent(event, props);
};

Same shape, two files. Resolve the platform variant by file extension (adapters.firebase.native.ts via Metro's .native.ts resolution, or a Platform.OS switch) so the registry, track fan-out, consent gate, taxonomy, and useAnalytics hook never change across platforms. Firebase's event-name rules (snake_case, lowercase, ≤ 40 chars) line up with the taxonomy rules in §2, so the canonical ANALYTICS_EVENTS constants are valid Firebase event names as-is. Gate initFirebaseAnalytics() on consent (§6) — Firebase also exposes setAnalyticsCollectionEnabled(false) to harden the opt-out.

Why this shape: analytics is the last thing that should crash an app. A vendor script that fails to load, a global that isn't there yet, an adapter that throws on a malformed prop — all are contained. The registry being exported and mutable makes dispatch unit-testable without mounting any provider.


4. The provider + hook (SSR-safe entry)

A 'use client' context exposes track through useAnalytics(). Outside a provider (tests, server) it returns a no-op, so instrumented components never throw in isolation.

// providers/AnalyticsProvider.tsx
"use client";
import { createContext, useContext, useMemo, type ReactNode } from "react";
import { track as trackEvent } from "@/services/analytics";
import type { AnalyticsEvent } from "@/constants/analytics";

interface AnalyticsContextValue {
  track: (event: AnalyticsEvent, props?: Record) => void;
}
const AnalyticsContext = createContext(null);

export function AnalyticsProvider({ children }: { children: ReactNode }) {
  // track is module-level and stable → memoize once, never re-render consumers.
  const value = useMemo(
    () => ({ track: trackEvent }),
    [],
  );
  return (
    
      {children}
    
  );
}

const NOOP: AnalyticsContextValue = { track: () => undefined };
export function useAnalytics(): AnalyticsContextValue {
  return useContext(AnalyticsContext) ?? NOOP; // safe outside a provider / on server
}
// a tracked leaf — Server Components can't use the hook, so wrap in a thin client component
"use client";
export function TrackedGithubLink({ href, children }: Props) {
  const { track } = useAnalytics();
  return (
     track(ANALYTICS_EVENTS.GITHUB_CLICK, { url: href })}
    >
      {children}
    
  );
}

The provider does no work during rendertrack is stable and adapters guard their own window access — so it's safe to mount at the root, including in SSR/RSC trees.


5. Real-user Core Web Vitals (the lab/field loop)

Report field vitals through the same fan-out. This is the complement to the lighthouse skill: the lab gate sets the budget; the field tells you whether real users hit it.

// services/analytics/web-vitals.ts
import { onLCP, onINP, onCLS, onFCP, onTTFB, type Metric } from "web-vitals";
import { track } from "./track";

export function reportWebVitals(): void {
  const send = (m: Metric) =>
    track("web_vital" as AnalyticsEvent, {
      name: m.name, // LCP | INP | CLS | FCP | TTFB
      value: Math.round(m.name === "CLS" ? m.value * 1000 : m.value),
      rating: m.rating, // good | needs-improvement | poor
      id: m.id,
    });
  onLCP(send);
  onINP(send);
  onCLS(send);
  onFCP(send);
  onTTFB(send);
}
  • Call reportWebVitals() once on the client (e.g. in the analytics provider's effect, or Next.js useReportWebVitals).
  • Use the same metrics and thresholds as the lighthouse skill (LCP ≤ 2500, INP ≤ 200, CLS ≤ 0.1) so lab and field speak the same language.
  • Lab budget green + field "poor" = a gap between your test conditions and real devices/networks — exactly what field RUM exists to reveal.

6. Consent and privacy gating

Telemetry fires only after opt-in, checked once at the fan-out boundary (§3) — not duplicated at every call site.

// services/analytics/consent.ts
let granted = false; // hydrate from a stored consent cookie/localStorage on init
export function setConsent(value: boolean): void {
  granted = value;
}
export function hasConsent(): boolean {
  return granted;
}

Hard rules:

  • track() early-returns when consent is absent — no events, no vitals, no error PII before opt-in.
  • Keep props PII-light: ids and enums, not emails/names/free text. Treat anything user-entered as sensitive.
  • Respect "Do Not Track" / regional regimes (GDPR/CCPA) by defaulting consent to false where required.
  • Error reports must scrub PII before leaving the device.

7. Error reporting at boundaries

Caught render errors and unhandled rejections go through the same fan-out (or a dedicated Sentry adapter), at deliberate boundaries — not a global swallow.

// error/ErrorBoundary.tsx (essence)
componentDidCatch(error: Error, info: ErrorInfo) {
  track("client_error" as AnalyticsEvent, {
    message: error.message, component: info.componentStack?.split("\n")[1]?.trim(),
  }); // or sentryAdapter(error, info)
}
  • Place boundaries at route/segment level (per the frontend-architecture page-directory model), so a crash degrades one surface, not the app.
  • Pair with the data layer's typed ApiError (frontend-data-contracts §6): report unexpected errors; expected ones (validation, 404) are handled, not reported as crashes.

8. Provider & framework adapters

The taxonomy + fan-out are constant; each provider is one window-guarded adapter.

| Provider | Adapter call | | ---------------------- | ---------------------------------------------------------------------------- | | Firebase (web) | logEvent(analytics, name, props) (firebase/analytics, lazy browser init) | | Firebase (RN/Expo) | analytics().logEvent(name, props) (@react-native-firebase/analytics) | | GA4 | window.gtag("event", name, props) | | Microsoft Clarity | window.clarity("event", name) | | PostHog | window.posthog?.capture(name, props) | | OpenPanel | op("track", name, props) or op.track(name, props) | | Sentry | Sentry.captureException(error) (error adapter) |

| Framework | Wiring | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Next.js | AnalyticsProvider in the root layout (client boundary); call initFirebaseAnalytics() in a client effect; vitals via useReportWebVitals. | | React + Vite / Remix | provider at app root; initFirebaseAnalytics() + reportWebVitals() in a top-level effect. | | Expo / React Native | swap the web-vitals source for RN performance APIs and the web provider scripts for native SDKs (**`

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.