@stackra/analytics
v2.0.0
Published
Analytics & tracking for the Stackra framework — a consent-gated, fan-out manager over pluggable destinations (GA4, console) and marketing pixels (Meta, TikTok, Snapchat), with auto-registration and React bindings.
Maintainers
Readme
@stackra/analytics
Analytics & tracking for the Stackra framework — a consent-gated, fan-out manager over pluggable destinations (GA4, console) and marketing pixels (Meta, TikTok, Snapchat), with auto-registration and React bindings.
Register
import { AnalyticsModule } from "@stackra/analytics";
@Module({
imports: [
// ConsentModule.forRoot(...) should be present so gating works.
AnalyticsModule.forRoot({
default: "console",
providers: {
console: { driver: "console" },
ga4: { driver: "ga4", measurementId: "G-XXXXXXX" }, // gated on `analytics`
"meta-pixel": { driver: "meta-pixel", pixelId: "123" }, // gated on `marketing`
},
}),
],
})
export class AppModule {}Consent gating
Each provider declares a consentCategory. The manager only dispatches to a
provider once consent for its category is granted (resolved from
@stackra/consent via Symbol.for('CONSENT_MANAGER') — no hard dependency).
Events emitted before consent are buffered and replayed per-provider as
categories are granted. With no consent manager wired the manager fails
closed (drops gated events) unless requireConsent: false.
Built-in category defaults: GA4 → analytics; Meta/TikTok/Snapchat →
marketing; console → ungated.
Custom / extra marketing providers
@AnalyticsProvider({ name: "amplitude" })
@Injectable()
export class AmplitudeProvider implements IAnalyticsProvider {
/* ... */
}
// or explicitly:
AnalyticsModule.forFeature(AmplitudeProvider);CSP
Script-injecting providers (GA4 + pixels) need their origins allow-listed or the browser blocks them. Derive the contributions from the same config so the CSP can't drift from the enabled providers:
import { AnalyticsModule, getAnalyticsCspPolicies } from "@stackra/analytics";
imports: [
CspModule.forRoot(cspConfig),
...getAnalyticsCspPolicies(analyticsConfig).map((p) =>
CspModule.forFeature(p),
),
AnalyticsModule.forRoot(analyticsConfig),
];The per-provider constants (GA4_CSP, META_PIXEL_CSP, TIKTOK_PIXEL_CSP,
SNAPCHAT_PIXEL_CSP) are also exported if you prefer to wire them manually.
React
const analytics = useAnalytics();
analytics.track("cta_clicked", { id: "hero" });
// auto page views:
usePageView(useLocation().pathname);Inject anywhere via ANALYTICS_MANAGER (from @stackra/contracts).
React Native
The ./native subpath ships two reporters wired into AnalyticsModule's
fan-out chain:
SegmentRnAnalyticsReporter— routes to@segment/analytics-react-native. Fans out downstream to whatever destinations the tenant has enabled on the Segment dashboard. Registered under the name"segment-rn".FirebaseRnAnalyticsReporter— routes to@react-native-firebase/analytics(Firebase Analytics + GA4 backing). Auto-configured via the app-bundledgoogle-services.json/GoogleService-Info.plist. Registered under the name"firebase-rn".
Both peers (@segment/analytics-react-native,
@react-native-firebase/analytics, @react-native-firebase/app) are OPTIONAL.
Each reporter loads its peer via a lazy dynamic import; when the peer is missing
every method is a silent no-op.
import { NativeAnalyticsModule } from "@stackra/analytics/native";
@Module({
imports: [
NativeAnalyticsModule.forRoot({
// Segment — optional; skip if you don't use Segment.
segmentRn: {
writeKey: process.env.SEGMENT_WRITE_KEY,
flushInterval: 30,
},
// Firebase — optional; auto-configured, so most apps just pass
// the collection toggle + tenant-scoped user properties.
firebaseRn: {
analyticsCollectionEnabled: true,
userProperties: { tenant_id: "01H...", app_variant: "sports" },
},
// Core AnalyticsModule fields still apply — consent gating,
// buffer limits, buffer-until-consent behaviour, etc.
bufferLimit: 200,
requireConsent: true,
}),
],
})
export class AppModule {}Because both reporters carry the @AnalyticsProvider metadata stamp, the core
AnalyticsProviderLoader picks them up at onApplicationBootstrap and merges
them into the manager's fan-out chain — no manual register(...) call and no
providers map entry needed. To trim the chain to a subset, set the module's
stack option (e.g. stack: ["segment-rn"] to disable Firebase).
Segment-only methods
The reporter exposes group(groupId, traits) and flush() — Segment concepts
absent from IAnalyticsProvider. Inject SegmentRnAnalyticsReporter directly
to call them:
constructor(
@Inject(SegmentRnAnalyticsReporter)
private readonly segment: SegmentRnAnalyticsReporter,
) {}
onJoinTenant(id: string): void {
this.segment.group(id, { plan: "pro" });
}iOS App Tracking Transparency
Both Segment and Firebase respect iOS 14.5+ ATT — the parent app is responsible
for calling the ATT prompt via @stackra/consent/native's AttService BEFORE
initialising the reporters. Without ATT approval, IDFA-based attribution
silently degrades but events still record.
