A typed, provider-agnostic analytics library for TypeScript applications. Define one event registry, create an application-owned client or server instance, and send the same events to any configured provider.
Read the full documentation for framework guides and provider-specific setup.
Give your coding agent Trakoo-specific integration guidance for typed events, client/server boundaries, providers, and framework setup:
npx skills add multiplehats/trakoo --skill trakooThe source is skills/trakoo/SKILL.md and follows the portable Agent Skills format used by skills.sh-compatible agents. See the Agent Skill docs for agent targeting, global installs, and manual setup.
- Event names and properties inferred from one registry
- Validator-free TypeScript definitions with optional Standard Schema validation
- Separate browser and server entry points
- Typed user traits and per-event user context
- Provider fan-out and routing
- Fresh, independently configured analytics instances
pnpm install trakooTrakoo core does not auto-install provider SDKs. Install only the SDKs required by the providers you use. For example:
pnpm install posthog-js posthog-nodeThe aggregate trakoo/providers/client and trakoo/providers/server entry points are safe to import before an SDK is installed. Constructing a provider is also safe; initializing it without its optional peer SDK throws a clear package-specific error.
The root trakoo entry point contains environment-neutral event helpers and shared types.
import { defineEvents, noProperties, typed } from 'trakoo';
export const appEvents = defineEvents({
userSignedUp: {
name: 'user_signed_up',
category: 'user',
properties: typed<{
userId: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
referralSource?: string;
}>()
},
featureUsed: {
name: 'feature_used',
category: 'engagement',
properties: typed<{
featureName: string;
duration?: number;
}>()
},
sessionStarted: {
name: 'session_started',
category: 'user',
properties: noProperties()
}
});typed<T>() gives you compile-time checking without a runtime validator. It verifies at runtime only that a property-bearing event receives a non-array object. Use noProperties() when callers must omit the properties argument entirely.
Factories and providers come from environment-specific subpaths. Pass the registry as a value; no event generic is needed.
import { createClientAnalytics } from 'trakoo/client';
import { PostHogClientProvider } from 'trakoo/providers/client';
import { appEvents } from './events';
export const analytics = createClientAnalytics({
events: appEvents,
providers: [
new PostHogClientProvider({
token: import.meta.env.VITE_POSTHOG_KEY,
api_host: import.meta.env.VITE_POSTHOG_HOST
})
],
debug: import.meta.env.DEV
});Each factory call returns a fresh instance. trakoo does not keep a global analytics singleton. Create and own the instance in your application, then import that owned instance where you track.
await analytics.track('user_signed_up', {
userId: 'user-123',
email: 'ada@example.com',
plan: 'pro'
});
await analytics.track('session_started');The registry drives autocomplete and rejects misspelled names, missing properties, extra properties, and a properties argument for session_started.
import { createServerAnalytics } from 'trakoo/server';
import { PostHogServerProvider } from 'trakoo/providers/server';
import { appEvents } from './events';
export const serverAnalytics = createServerAnalytics({
events: appEvents,
providers: [
new PostHogServerProvider({
apiKey: process.env.POSTHOG_API_KEY!,
host: process.env.POSTHOG_HOST
})
]
});Server tracking accepts user and request context per call:
await serverAnalytics.track('feature_used', {
featureName: 'export-data',
duration: 1500
}, {
userId: 'user-123',
user: {
email: 'ada@example.com',
traits: { plan: 'pro' }
},
context: {
page: { path: '/api/export' }
}
});
await serverAnalytics.shutdown();Call shutdown() before a serverless request or worker exits so providers can flush queued events.
Standard Schema is an interface implemented by validator libraries; it is not a validator runtime that trakoo requires. The primary API remains validator-free typed<T>(). When an event crosses an untrusted boundary, pass a compatible validator directly. Zod implements Standard Schema:
import { defineEvents } from 'trakoo';
import { z } from 'zod';
export const commerceEvents = defineEvents({
orderCompleted: {
name: 'order_completed',
category: 'conversion',
properties: z.object({
orderId: z.string(),
amount: z.coerce.number().positive()
})
}
});The schema's input type controls what track() accepts. Its output type controls the validated and transformed properties passed to providers. In this example, amount may be a coercible input, but providers always receive a positive number.
Validation failures are dropped by default. For strict handling, opt into throwing and report the normalized, payload-free error:
const analytics = createClientAnalytics({
events: commerceEvents,
providers: [/* ... */],
validation: {
onFailure: 'throw',
onError: (error) => reportValidationFailure(error)
}
});AnalyticsValidationError contains a code, event name, and normalized issue messages/paths. Validator issue messages are retained for onError and thrown errors, but the complete input payload is never attached to the error. With debug: true and no onError, the fallback warning deliberately omits issue messages and input values; it contains only the code, event name, and issue paths.
The error callback is awaited before the configured drop or throw policy is applied. Async schemas also mean concurrent track() calls can reach providers in validation-completion order rather than call order. Await calls sequentially if delivery order matters.
This validation policy applies only to event lookup and property validation. Initialization failures and provider failures keep their existing behavior.
Use another typed<T>() marker for custom traits. The factory still infers the event registry from events.
import { typed } from 'trakoo';
import { createClientAnalytics } from 'trakoo/client';
import { appEvents } from './events';
interface UserTraits {
email: string;
name: string;
plan: 'free' | 'pro' | 'enterprise';
company?: string;
}
export const analytics = createClientAnalytics({
events: appEvents,
userTraits: typed<UserTraits>(),
providers: [/* ... */]
});
analytics.identify('user-123', {
email: 'ada@example.com',
name: 'Ada Lovelace',
plan: 'pro'
});Client analytics remembers the identified user until reset(). Server analytics is stateless: pass userId and user with each track() call.
const analytics = createClientAnalytics({
events: appEvents,
providers: [
new PostHogClientProvider({ token: 'posthog-token' }),
{
provider: new BentoClientProvider({ siteUuid: 'bento-site' }),
methods: ['identify', 'track'],
events: ['user_signed_up']
},
new VisitorsClientProvider({ token: 'visitors-token' })
]
});Every configured provider receives eligible calls. Routing can restrict methods, exact event names, excluded events, or event-name patterns.
Extend the base class from the environment where the provider will run:
import {
BaseAnalyticsProvider,
type BaseEvent,
type EventContext
} from 'trakoo/client';
export class ConsoleProvider extends BaseAnalyticsProvider {
name = 'Console';
initialize() {}
identify(userId: string, traits?: Record<string, unknown>) {
console.log('identify', { userId, traits });
}
track(event: BaseEvent, context?: EventContext) {
console.log('track', { event, context });
}
pageView(properties?: Record<string, unknown>) {
console.log('page view', properties);
}
reset() {}
}See Creating Custom Providers for the full lifecycle.
| Import | Contents |
|---|---|
trakoo |
defineEvents, typed, noProperties, validation error, and shared types |
trakoo/client |
Client factory, browser analytics class, client-safe base provider exports |
trakoo/server |
Server factory, server analytics class, server-safe base provider exports |
trakoo/providers/client |
Browser provider implementations |
trakoo/providers/server |
Server provider implementations |
Do not import factories from the root or use a combined provider entry point.
Creates the branded runtime registry required by both factories. Duplicate wire names throw while the registry is created.
Declares an object-shaped compile-time input/output type without validating its fields at runtime.
Declares an event that must be tracked without a properties argument.
Requires config.events. Optional configuration includes providers, userTraits, validation, debug, and enabled. Returns a fresh client instance.
Requires config.events. Optional configuration includes providers, userTraits, validation, debug, enabled, and defaultContext. Returns a fresh server instance.
- Keep a single authoritative registry and pass that registry value to every analytics factory.
- Prefer
typed<T>(); add a Standard Schema validator only at boundaries that need runtime checking or transformation. - Model zero-property events with
noProperties(). - Create analytics and provider instances in application-owned modules; do not rely on hidden global state.
- Use client/server subpaths so environment-specific code stays out of the wrong bundle.
- Await server events and call
shutdown()when delivery must complete before the runtime exits. - Never send secrets or unnecessary personal data to analytics providers.
Contributions are welcome. Please open an issue or pull request with a focused description and tests where behavior changes.
