---
name: talvess-signups
description: Track confirmed account signups in Talvess analytics. Use when asked to connect signup tracking, send a Talvess signup event, or attribute new accounts to their traffic sources.
---

# Talvess signup tracking

Talvess counts a signup only when your server confirms that a new account was created. Your job is to send one server request to Talvess right after account creation succeeds, and to forward optional browser context so the signup can be credited to the visit that brought the person in.

The prompt that loaded this skill gives you three values. Ask for any that are missing before you write code:

- `TALVESS_ORIGIN`: the origin serving Talvess, for example `https://talvess.example.com`.
- `WEBSITE_ID`: the Talvess website ID (a UUID).
- `TALVESS_EVENTS_KEY`: a server key. Never ask the user to paste the key into chat and never hardcode it. Read it from the server environment only.

## 1. Find the account creation path

Locate the server code that creates a new account in this project: a registration handler, an auth provider callback such as `createUser` or `onUserCreated`, or a database insert for users. Track only paths that create a new account.

Do not send a signup for logins, button clicks, form submissions, success page visits, failed registrations, or repeated callbacks for an existing account.

## 2. Add the server environment variables

Add `TALVESS_ORIGIN` and `TALVESS_EVENTS_KEY` to the project's server environment configuration and its example env file (with an empty key value). Do not expose them to the browser: no `NEXT_PUBLIC_`, `VITE_`, or similar public prefixes.

## 3. Send the signup event from the server

After the account has been created successfully, send:

```http
POST {TALVESS_ORIGIN}/api/events
Authorization: Bearer {TALVESS_EVENTS_KEY}
Content-Type: application/json

{
  "name": "signup",
  "eventId": "signup:<opaque account id>",
  "customerId": "<same opaque account id used on payments>",
  "occurredAt": "<account creation time, ISO 8601 with timezone>",
  "context": { "websiteId": "...", "visitorId": "...", "sessionId": "..." }
}
```

Rules:

- `eventId` must be stable for the account so retries count once. Use the opaque account ID. Never use an email, name, or other personal data.
- `context` is optional. Omit it entirely when the browser did not supply a valid one.
- Include `customerId` on new integrations when it is the same opaque account ID used on payments. Preserve the exact original fields when replaying an existing signup; adding a field that differs from its saved receipt can return `409`.
- Serialize the original account creation time and payload once. Retry network failures, `408`, `429`, `500`, `502`, `503`, and `504` with bounded exponential backoff and jitter. Honor `Retry-After` in seconds or as an HTTP date. Do not automatically retry permanent errors such as `400`, `401`, `409`, `501`, or `505`.
- A valid `201` or `200` response includes an `event.id` receipt. `202 { "ignored": true }` means signup tracking is disabled and no signup was recorded; report that configuration state without retrying it. A status code alone, HTML response, or malformed receipt is not proof of collection.
- Tracking must never block or roll back account creation. Schedule delivery with the runtime's supported post-response hook or an existing durable worker after the account commits. Catch signup tracking errors and log only safe status/error codes, never keys, the full event, or personal data. An unawaited serverless request is not reliable delivery.
- Prefer a small shared helper such as `trackSignup(account, context)` placed with the project's other server utilities, and match the project's language, HTTP client, and logging conventions.

Reference helper for a Node.js server with built-in `fetch`:

This helper makes up to four attempts, times out each request and response body read within five seconds, and bounds delivery to 25 seconds including backoff. The host's post-response duration must cover that budget; `after()` remains subject to the route duration. If `Retry-After` exceeds the budget, it reports failure for later replay instead of sleeping beyond the deadline. Exhaustion is best effort, not a durable queue: retain a failed job in existing infrastructure, or replay the exact original event from authoritative signup records after correcting the failure. Do not reset old occurrence times to bypass the API's 30-day limit. If the original context was not retained, do not invent it.

```js
async function readTalvessResponse(response) {
  const reader = response.body?.getReader()
  if (!reader) return null
  const decoder = new TextDecoder()
  let bytes = 0
  let text = ""
  // Body reads share fetch's timeout. Limit the response to 8 KiB.
  while (true) {
    const chunk = await reader.read()
    if (chunk.done) break
    bytes += chunk.value.byteLength
    if (bytes > 8192) {
      await reader.cancel().catch(() => {})
      return null
    }
    text += decoder.decode(chunk.value, { stream: true })
  }
  try { return JSON.parse(text + decoder.decode()) } catch { return null }
}

// Bounded best effort: replay exhausted deliveries from your source records.
// Your host's post-response duration must cover this 25-second delivery budget.
async function deliverTalvessEvent(body) {
  const key = process.env.TALVESS_EVENTS_KEY
  if (!key) return { status: "failed", code: "NOT_CONFIGURED", retryable: false }
  const deadline = Date.now() + 25_000
  let failure = { status: "failed", retryable: true }
  for (let attempt = 0; attempt < 4; attempt++) {
    const remaining = deadline - Date.now()
    if (remaining <= 0) return failure
    let retryAt = 0
    let response
    try {
      response = await fetch(new URL("/api/events", process.env.TALVESS_ORIGIN), {
        method: "POST",
        headers: {
          authorization: `Bearer ${key}`,
          "content-type": "application/json",
        },
        body,
        redirect: "error",
        signal: AbortSignal.timeout(Math.min(5000, remaining)),
      })
      // Preserve known status and retry deadlines even if the body disconnects.
      const retryableStatus = [408, 429, 500, 502, 503, 504].includes(response.status)
      failure = { status: "failed", httpStatus: response.status,
        retryable: retryableStatus || [200, 201, 202].includes(response.status) }
      const header = response.headers.get("retry-after")
      if (header) {
        const seconds = Number(header)
        retryAt = Number.isFinite(seconds) ? Date.now() + Math.max(0, seconds * 1000)
          : Date.parse(header) || 0
      }
      const payload = await readTalvessResponse(response)
      const object = payload && typeof payload === "object" ? payload : null
      if ([200, 201, 202].includes(response.status)) {
        const receipt = object && "event" in object ? object.event :
          object && "transaction" in object ? object.transaction : null
        if (response.status === 202 && object && "ignored" in object && object.ignored === true)
          return { status: "ignored", httpStatus: 202 }
        if (response.status !== 202 && receipt && typeof receipt === "object" && "id" in receipt &&
          typeof receipt.id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(receipt.id))
          return { status: "recorded", httpStatus: response.status }
        return { status: "failed", httpStatus: response.status, code: "INVALID_RECEIPT", retryable: false }
      }
      const code = object && "code" in object &&
        typeof object.code === "string" && /^[A-Z_]{1,64}$/.test(object.code)
          ? object.code : undefined
      const retryable = retryableStatus ||
        (response.status === 409 && code === "PAYMENT_NOT_FOUND")
      failure = { status: "failed", httpStatus: response.status, ...(code ? { code } : {}), retryable }
      if (!retryable) return failure
    } catch {
      if (!response) failure = { status: "failed", retryable: true }
    }
    if (!failure.retryable) return failure
    const delay = Math.max(500 * 2 ** attempt + Math.floor(Math.random() * 250), retryAt - Date.now())
    if (attempt === 3 || Date.now() + delay >= deadline) return failure
    await new Promise((resolve) => setTimeout(resolve, delay))
  }
  return failure
}

export async function trackSignup(account, browserContext) {
  try {
    // Preserve these original fields and the serialized body on every replay.
    // Update an existing signup sender; do not install a second one.
    const body = JSON.stringify({
      name: "signup",
      eventId: "signup:" + account.id,
      customerId: account.id, // Same opaque ID used on payments, never an email.
      context: browserContext ?? undefined,
      occurredAt: new Date(account.createdAt).toISOString(),
    })
    const result = await deliverTalvessEvent(body)
    if (result.status === "ignored") {
      console.warn("Talvess signup tracking is disabled; signup was not recorded")
    } else if (result.status === "failed") {
      console.error("Talvess signup not delivered; replay the original event after resolving delivery", result)
    }
  } catch {
    console.error("Talvess signup tracking failed; check the original account data")
  }
}
// Schedule after the account commits, so tracking never delays the signup response.
// Next.js: import { after } from "next/server"
// after(() => trackSignup(account, browserContext))
// Other backends: use their supported post-response hook or existing job worker.
// Keep failed delivery visible and replay from original records; no durable queue is added.
```

## 4. Forward browser context from the signup form

The Talvess tracker script must already be installed on the website. In the browser code that submits signup, read the context and send it to your own backend with the registration request:

```js
const talvessContext = window.talvess?.getContext(WEBSITE_ID) ?? null
```

Pass it through to `trackSignup`. A missing tracker, blocked storage, or `null` context must never prevent signup. Validate on the server that it contains only `websiteId` and `visitorId` UUID strings and an optional `sessionId` UUID, with the expected website ID, before forwarding it, and drop it otherwise.

For OAuth or other redirects, save the context with the project's existing server-side auth transaction and read it back after the provider callback. Never replace the provider's `state`, nonce, or CSRF checks with it.

## 5. Marketing site and app on different origins

Only when the marketing site and the signup app run on different origins, load `{TALVESS_ORIGIN}/signup-context.js` on both:

- Marketing site, in the signup link's click handler: `location.assign(window.talvessSignupContext.attach(signupUrl, WEBSITE_ID) ?? signupUrl)`.
- Signup app, at the earliest startup: `const context = window.talvessSignupContext.consume(WEBSITE_ID)`, then later `window.talvessSignupContext.get(WEBSITE_ID) ?? context`.

Never put a server key in a URL.

## 6. Verify

1. Run the project's typecheck, lint, and tests.
2. Create a test account locally with the env vars set and confirm the request returns `201` (or `200` for an identical retry).
3. Tell the user to open Track signups in Talvess Analytics and check receipt. Summarize the files you changed and where the event is sent.
