Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e4f6706
docs: design Trakoo agent skill
multiplehats Jul 22, 2026
41be03b
docs: expand agent skill framework coverage
multiplehats Jul 22, 2026
ba4a074
docs: plan Trakoo agent skill implementation
multiplehats Jul 22, 2026
9214e06
docs: correct agent skill baseline evaluation
multiplehats Jul 22, 2026
7b83c8b
feat: add Trakoo agent skill
multiplehats Jul 22, 2026
fa3d42a
docs: add Trakoo provider skill reference
multiplehats Jul 22, 2026
a7d2904
docs: add Trakoo framework skill reference
multiplehats Jul 22, 2026
55bd45b
docs: document Trakoo agent skill installation
multiplehats Jul 22, 2026
86ffa4c
fix: harden agent skill lifecycle guidance
multiplehats Jul 22, 2026
f67b541
docs: correct Trakoo skill lifecycle guidance
multiplehats Jul 22, 2026
388b81c
docs: scope server analytics lifecycle
multiplehats Jul 22, 2026
b95cbbe
docs: clarify EmitKit skill initialization
multiplehats Jul 22, 2026
35894bc
docs: rewrite agent skill for v1 event registry
multiplehats Jul 22, 2026
c8fc311
test: strengthen v1 agent skill contracts
multiplehats Jul 22, 2026
689f48e
docs: harden v1 integration guidance
multiplehats Jul 22, 2026
ca9fbf6
docs: secure TanStack purchase tracking example
multiplehats Jul 22, 2026
6ad1685
docs: drop superseded skill plans
multiplehats Jul 22, 2026
edbf18f
docs: harden skill integration safety
multiplehats Jul 22, 2026
839b687
docs: fix v1 skill runtime boundaries
multiplehats Jul 22, 2026
b071629
docs: add v1 skill release gate
multiplehats Jul 22, 2026
b440f78
docs: document agent skill installation
multiplehats Jul 27, 2026
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
10 changes: 10 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ A highly typed, zero-dependency, provider-agnostic analytics library for TypeScr
- 🔌 [Providers](https://stacksee-analytics.vercel.app/docs/providers)
- 💡 [Core Concepts](https://stacksee-analytics.vercel.app/docs/core-concepts)

## Agent Skill

Give your coding agent Trakoo-specific integration guidance for typed events, client/server boundaries, providers, and framework setup:

```bash
npx skills add multiplehats/trakoo --skill trakoo
```

The source is [`skills/trakoo/SKILL.md`](./skills/trakoo/SKILL.md) and follows the portable Agent Skills format used by skills.sh-compatible agents. See the [Agent Skill docs](https://stacksee-analytics.vercel.app/docs/agent-skill) for agent targeting, global installs, and manual setup.

## Features

- 🎯 **Type-safe events**: Define your own strongly typed events with full IntelliSense support
Expand Down
201 changes: 201 additions & 0 deletions skills/trakoo/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
---
name: trakoo
description: Use when adding, configuring, or troubleshooting Trakoo analytics in TypeScript applications, including typed or validated events, browser or server tracking, PostHog, OpenPanel, Bento, Pirsch, EmitKit, Visitors, Proxy, provider routing, user identification, or framework integration.
---

# Trakoo Integration

Trakoo is a typed, provider-agnostic analytics library. Define one runtime event registry, pass it to every analytics factory, and preserve the browser/server boundary.

## Inspect the application first

Detect the package manager, framework, installed `trakoo` version, browser/server entry points, analytics dependencies, and verification commands. Decide whether each event is a browser interaction, an authoritative server outcome, or both.

Use the consuming project's installed Trakoo declarations as the source of truth. Consult the current documentation at https://trakoo.co. If the installed declarations differ, explain the version mismatch instead of silently upgrading or inventing an API.

- Read [references/events-and-validation.md](references/events-and-validation.md) when defining events, adding Zod or another Standard Schema validator, configuring validation failures, or using propertyless events and custom traits.
- Read [references/providers.md](references/providers.md) when choosing providers, routing events, using Proxy, or building a custom provider.
- Read only the matching section of [references/frameworks.md](references/frameworks.md) for Next.js, SvelteKit, TanStack Start, Astro, or framework-neutral integration.

## Integration workflow

1. Install `trakoo`, the selected providers' optional SDKs, and a validator only when runtime validation is wanted.
2. Define and export one shared registry with `defineEvents()`.
3. Put client and server analytics in separate modules and pass `events: appEvents` to each factory.
4. Track where the event becomes true: interactions in browser handlers; payments, signups, jobs, and other authoritative outcomes on the server.
5. Apply identity, validation, and delivery lifecycle rules.
6. Run the consuming project's formatter, type checker, relevant tests, and production build.

## Shared event registry

Root helpers are environment-neutral and safe to import from shared code:

```ts
import { defineEvents, noProperties, typed } from "trakoo";

export const appEvents = defineEvents({
ctaClicked: {
name: "cta_clicked",
category: "engagement",
properties: typed<{ location: "hero" | "pricing" }>(),
},
purchaseCompleted: {
name: "purchase_completed",
category: "conversion",
properties: typed<{
orderId: string;
amount: number;
currency: string;
}>(),
},
sessionStarted: {
name: "session_started",
category: "user",
properties: noProperties(),
},
});
```

Registry keys organize application source; each `name` is the stable value emitted to providers. Use a Standard Schema-compatible validator directly for runtime validation and transformation. See the event reference for a complete Zod example.

## Browser module

```ts
import { typed } from "trakoo";
import { createClientAnalytics } from "trakoo/client";
import { PostHogClientProvider } from "trakoo/providers/client";
import { appEvents } from "./events";

interface BrowserUserTraits {
email: string;
plan: "free" | "pro";
}

export const analytics = createClientAnalytics({
events: appEvents,
userTraits: typed<BrowserUserTraits>(),
providers: [
new PostHogClientProvider({
// Browser-public PostHog project key (phc_...), never a personal API key.
token: import.meta.env.VITE_POSTHOG_PROJECT_KEY,
}),
],
});

const analyticsReady = analytics.initialize();

export async function identifyUser(userId: string, traits: BrowserUserTraits) {
await analyticsReady;
analytics.identify(userId, traits);
}
```

Every factory call creates a fresh, registry-bound instance owned by the application. Retain its initialization promise and await readiness before the first `identify()` or identity-sensitive event because some providers ignore identity calls before their SDK is ready. Login, signup, and restored-session/bootstrap flows must await the identity helper; call `analytics.reset()` on logout.

At restored-session bootstrap, identify before enabling identity-sensitive events:

```ts
const restoredSession = await restoreSession();

if (restoredSession.user) {
await identifyUser(restoredSession.user.id, {
email: restoredSession.user.email,
plan: restoredSession.user.plan,
});
}

// Enable identity-sensitive events only after this bootstrap completes.
```

`track()` returns `Promise<void>`. Await work whose completion matters; use `void analytics.track(...)` for explicitly non-critical browser analytics rather than blocking navigation.

## Server module and critical event

```ts
import { typed } from "trakoo";
import { createServerAnalytics } from "trakoo/server";
import { PostHogServerProvider } from "trakoo/providers/server";
import { appEvents } from "./events";

interface ServerUserTraits {
plan: "free" | "pro";
}

function createRequestAnalytics() {
return createServerAnalytics({
events: appEvents,
userTraits: typed<ServerUserTraits>(),
providers: [
new PostHogServerProvider({
// PostHog project capture key (phc_...), not a personal API key.
apiKey: process.env.POSTHOG_PROJECT_KEY!,
}),
],
});
}

export async function trackPurchase(input: {
orderId: string;
amount: number;
currency: string;
userId: string;
email: string;
plan: "free" | "pro";
}) {
const analytics = createRequestAnalytics();
try {
await analytics.track(
"purchase_completed",
{
orderId: input.orderId,
amount: input.amount,
currency: input.currency,
},
{
userId: input.userId,
user: {
email: input.email,
traits: { plan: input.plan },
},
},
);
} finally {
await analytics.shutdown();
}
}
```

Server analytics is stateless across users: pass user context with each event and await critical events. For a fresh request-scoped provider/analytics pair, shut down that same pair in `finally`. A reusable instance shuts down only at application or process teardown. Some providers flush buffered events during shutdown; others only clear state. Inspect the selected provider's installed implementation before choosing the lifecycle. Use the platform's `waitUntil` only for explicitly non-critical work.

## Imports and ownership

| Concern | Correct pattern |
|---|---|
| Event helpers and shared types | `trakoo` (environment-neutral) |
| Browser factory | `trakoo/client` |
| Server factory | `trakoo/server` |
| Browser providers | `trakoo/providers/client` |
| Server providers | `trakoo/providers/server` |
| Browser identity | Await initialization before first identity transition; reset on logout |
| Server identity | Pass request user context on every call |
| Critical server delivery | Await `track()`; shutdown follows provider behavior and instance ownership |

Never import a server provider, secret, or unprefixed server environment variable into browser code. Do not use a nonexistent `trakoo/providers` aggregate.

## Verification

Run the consuming project's type checker and narrowest relevant tests. Run its production build when client/server bundling or environment variables changed. Recheck installed declarations when an import, option, event property, or provider constructor fails.

**Maintainer release gate:** This skill must not be published until PR #32's migrated implementation lands and representative consumer fixtures are compiled and typechecked against the real built package, not fabricated declarations. Fixtures must cover root helpers and registry; direct Zod transformation and distinct input/output types; Valibot or ArkType; client and server factories sharing `events`; browser identify traits and nested server traits; a propertyless client call; a propertyless server-options call; and validation `"drop"` and `"throw"`.

## Common mistakes

- Defining calls before the shared runtime registry.
- Forgetting `events: appEvents` in either factory.
- Repeating event generics instead of allowing registry inference.
- Using `typed<T>()` when untrusted runtime input needs a validator.
- Sharing stateful browser identity logic with stateless server analytics.
- Sending all methods and events to providers with different requirements instead of routing them.
- Installing every optional SDK instead of only the selected provider and validator packages.
- Calling `identify()` before client provider initialization or forgetting logout reset.
- Pairing a reusable server instance with per-event shutdown.
167 changes: 167 additions & 0 deletions skills/trakoo/references/events-and-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Events and Validation Reference

Use one shared runtime registry for browser and server analytics. The object keys organize source code; each definition's `name` is the emitted event name accepted by `track()` and sent to providers.

## Define events

Install only the chosen validator when runtime validation is needed. Trakoo does not require one:

```bash
npm install trakoo zod
```

```ts
import { defineEvents, noProperties, typed } from "trakoo";
import { z } from "zod";

export const appEvents = defineEvents({
buttonClicked: {
name: "button_clicked",
category: "engagement",
properties: typed<{
buttonId: string;
location?: "hero" | "pricing";
}>(),
},
purchaseCompleted: {
name: "purchase_completed",
category: "conversion",
properties: z.object({
orderId: z.string(),
amount: z.coerce.number().positive(),
currency: z.string().length(3),
}),
},
sessionStarted: {
name: "session_started",
category: "user",
properties: noProperties(),
},
});
```

The three property modes are:

| Definition | Compile-time type | Runtime behavior |
|---|---|---|
| `typed<T>()` | Input and output are `T` | No runtime validation |
| Standard Schema validator | Inferred validator input and output | Validates and normalizes before routing |
| `noProperties()` | No properties argument | Normalizes omitted input to an empty provider object |

Recent compatible Zod, Valibot, ArkType, and other Standard Schema implementations work directly with no Trakoo adapter. Install only the chosen validator; do not add validator-specific Trakoo configuration. Trakoo itself remains validator-optional.

`typed<T>()` is for trusted, type-checked application values. It does not validate JavaScript, JSON, form data, or other untrusted input at runtime. Use a schema when that boundary needs validation. Duplicate emitted names are a registry initialization error.

## Schema input and provider output

A schema can accept one input shape and return a normalized provider output. In the example, Zod coercion accepts a numeric string and providers receive a number:

```ts
await analytics.track("purchase_completed", {
orderId: "order_456",
amount: "19.95",
currency: "EUR",
});
```

Trakoo awaits the Standard Schema result, rejects invalid or non-object output under the configured policy, and routes the successful output—not the original input—to every selected provider. Transformations, coercion, stripping, and sanitization therefore happen once before routing.

## Propertyless calls

Client calls omit argument two:

```ts
await analytics.track("session_started");
```

For server analytics, server options go directly in argument two:

```ts
await serverAnalytics.track(
"session_started",
{
userId: "user_123",
user: {
email: "ada@example.com",
traits: { plan: "pro" },
},
},
);
```

`userId` belongs directly in server track options. Identity fields such as `email` stay top-level in `user`, while application traits belong under `user.traits`. Do not pass a raw custom-traits object as `user`.

Never pass an `undefined` properties placeholder. A client second argument, a server `undefined` placeholder, or properties supplied through untyped JavaScript is `invalid_properties` under the validation-failure policy.

## Factory inference and user traits

Both factories require the same registry value and infer the event map:

```ts
import { typed } from "trakoo";
import { createClientAnalytics } from "trakoo/client";
import { createServerAnalytics } from "trakoo/server";
import { appEvents } from "./events";

interface BrowserUserTraits {
email: string;
plan: "free" | "pro";
}

interface ServerUserTraits {
plan: "free" | "pro";
}

const analytics = createClientAnalytics({
events: appEvents,
userTraits: typed<BrowserUserTraits>(),
providers: clientProviders,
});

const serverAnalytics = createServerAnalytics({
events: appEvents,
userTraits: typed<ServerUserTraits>(),
providers: serverProviders,
});
```

Use separate browser and server trait contracts. The browser marker must declare every object-literal field sent to `identify()`, including canonical identity values such as `email` and application values such as `plan`. The server marker describes only the custom values nested under `user.traits`; keep identity fields such as `email` at the top level of `user`. The optional `userTraits` marker is type-only and is neither validated nor sent to providers as configuration.

## Validation timing

Every `track()` variant returns `Promise<void>`. Validation finishes before provider routing begins, so a failed event is never partially delivered. Successful normalized output is shared by every selected provider.

Standard Schema validation may be asynchronous. Concurrent calls are not guaranteed to reach providers in call order; await calls sequentially when order matters. Provider delivery remains parallel and isolated after validation.

## Failure policy and security

`onFailure` defaults to `"drop"` on both client and server. Invalid events resolve without delivery so analytics validation does not fail a successful business operation. Strict tests or workflows can opt into rejection:

```ts
const analytics = createServerAnalytics({
events: appEvents,
providers: serverProviders,
validation: {
onFailure: "throw",
onError(error) {
reportValidationFailure({
code: error.code,
eventName: error.eventName,
paths: error.issues.map((issue) => issue.path),
});
},
},
});
```

`AnalyticsValidationError` reports the emitted event name, normalized issue information, and one of `unknown_event`, `invalid_properties`, `validator_failure`, or `invalid_output`. It never retains the submitted properties object or validator exception. Its normalized issues may include validator-provided messages, and those messages can contain application values.

When configured, `onError` receives the error exactly once. A throwing or rejected callback cannot change the selected drop/throw policy. Select and sanitize fields before forwarding a validation failure to application observability; the example sends only the code, event name, and normalized paths rather than the whole error.

Default debug logging exposes only sanitized metadata such as the code, event name, and normalized paths. It never logs raw messages or input.

This validation policy does not redefine provider-delivery or initialization errors. Those retain their existing behavior, so `track()` can still reject for reasons outside event validation.

## Verification

Run the application's type checker and production build. Add runtime tests for schema success, transformation, and rejection. Confirm invalid events do not reach any provider and normalized output reaches every routed provider.
Loading
Loading