@resq-sw/analytics
v0.4.0
Published
Unified PostHog + GA4 analytics client for the ResQ platform — cross-subdomain, lazy-loaded, with typed events
Maintainers
Readme
@resq-sw/analytics
Unified PostHog + GA4 analytics client for the ResQ platform. Built for cross-subdomain identity (resq.software ↔ research.resq.software ↔ viz.resq.software), lazy-loaded so it never sits on the LCP critical path, and typed events you can extend per-app.
Install
bun add @resq-sw/analytics posthog-js
# or
npm install @resq-sw/analytics posthog-jsposthog-js, react, and react-dom are optional peer dependencies — only install what your consumer actually uses.
Quick start (Next.js App Router)
// next.config.ts
import { withAnalyticsRewrites } from "@resq-sw/analytics/next";
export default withAnalyticsRewrites({
// ...your existing config
});// app/providers.tsx
"use client";
import { AnalyticsProvider } from "@resq-sw/analytics/react";
const config = {
posthog: {
key: process.env.NEXT_PUBLIC_POSTHOG_KEY!,
host: "/ingest",
uiHost: "https://us.posthog.com",
},
ga4: { measurementId: process.env.NEXT_PUBLIC_GA4_ID! },
cookieDomain: ".resq.software",
};
export const Providers = ({ children }: { children: React.ReactNode }) => (
<AnalyticsProvider config={config}>{children}</AnalyticsProvider>
);"use client";
import { useAnalytics } from "@resq-sw/analytics/react";
export const RequestBriefingButton = () => {
const { track } = useAnalytics();
return (
<button onClick={() => track("briefing_requested", { tier: "defense" })}>
Request a briefing
</button>
);
};Typed events
Extend AnalyticsEvents once per app to make track() calls type-safe:
declare module "@resq-sw/analytics" {
interface AnalyticsEvents {
briefing_requested: { tier: "civilian" | "defense" | "allied" };
cta_clicked: { id: string; section: string };
research_paper_opened: { slug: string; locale: string };
}
}After this, track("briefing_requested", { tier: "civilian" }) type-checks; track("briefing_requested", { tier: "wrong" }) does not.
API
Core (@resq-sw/analytics)
| Export | Purpose |
|---|---|
| initAnalytics(config) | Boot the singleton. Idempotent. |
| track(event, props?) | Fan out to PostHog + GA4. |
| identify(userId, traits?) | Bind an identity to the current session. |
| pageview(url?) | Manual SPA pageview. |
| reset() | Clear identity + provider state. Use on sign-out. |
| analytics | The singleton, if you need direct access. |
| inferCookieDomain(domains) | Build .resq.software from a domain allow-list. |
React (@resq-sw/analytics/react)
| Export | Purpose |
|---|---|
| <AnalyticsProvider config deferUntilIdle?> | Initialises the singleton on mount. deferUntilIdle (default true) waits for requestIdleCallback. |
| useAnalytics() | Returns { track, identify, reset, pageview, analytics }. |
Next (@resq-sw/analytics/next)
| Export | Purpose |
|---|---|
| withAnalyticsRewrites(config, opts?) | Adds /ingest/* PostHog reverse-proxy rewrites. |
| ga4Stream(measurementId, domains?) | Build a GA4ProviderConfig with cross-subdomain linker domains. |
Cross-subdomain identity
For ResQ's three surfaces to share a single distinct_id:
- Cookie domain. Set
cookieDomain: ".resq.software"(or callinferCookieDomain([...])). - Reverse proxy. Each subdomain's
next.config.tscallswithAnalyticsRewrites(...)so events ingest at<subdomain>/ingest/*, not*.posthog.com. - GA4 linker. Pass
domains: ["resq.software", "research.resq.software", "viz.resq.software"]so GA4 stops counting cross-subdomain navigation as referral traffic. - Same Measurement ID + PostHog key across all three apps.
Performance posture
- Zero runtime dependencies;
posthog-jsis loaded via dynamicimport()insideinit(). <AnalyticsProvider deferUntilIdle>waits forrequestIdleCallbackbefore booting.person_profiles: "identified_only"is set by default, so anonymous traffic doesn't burn PostHog units.
License
Apache-2.0
