AccoilAccoil Developer Docs
Examples

Fizzy by 37signals

Product Tracking Skills output for Fizzy — a kanban-style issue tracker by 37signals.

Fizzy — Kanban as it should be

About Fizzy

Fizzy is a kanban-style issue tracker built by 37signals — the team behind Basecamp and HEY. Teams create boards of cards and move them through workflow columns, collaborating via comments, assignments, and mentions.

Fizzy isn't open source, but 37signals make the source code accessible. That makes it an ideal example app for running Product Tracking Skills — it's a real, substantial Rails codebase that everyone can look at, and it doesn't ship with any product analytics built in.

What you're looking at

We ran Product Tracking Skills v1.0.2 on the Fizzy codebase with Segment as the analytics destination. The files below are the real .telemetry/ artifacts the skills produced — product model, delta, tracking plan, audit, instrumentation guide, and implementation details.

Everything runs locally in your AI agent tool. Your code never leaves your machine — nothing is sent back to us. The skills are markdown files with reference libraries. They're on GitHub — go read them.


Product Model

File: .telemetry/product.mdGenerated by the Model skill

The skills start by scanning the codebase to understand what the product does before deciding what to track.

.telemetry/product.md
# Product: Fizzy

**Last updated:** 2026-03-11
**Method:** codebase scan + conversation

## Product Identity
- **One-liner:** Teams create boards of cards to track issues and move them through workflow columns — from triage to closed — collaborating via comments, assignments, and mentions along the way.
- **Category:** b2b-saas
- **Product type:** B2B — group hierarchy and account-level tracking apply.
- **Collaboration:** multiplayer

## Business Model
- **Monetization:** freemium (free tier with paid upgrades)
- **Pricing tiers:** Free (1,000 cards, 1 GB storage), Unlimited ($20/mo, unlimited cards, 5 GB storage), Unlimited + Extra Storage ($25/mo, unlimited cards, 500 GB storage)
- **Billing integration:** Stripe (via Queenbee subscription management)

## Tech Stack
- **Primary language:** Ruby
- **Framework:** Rails (edge/main branch)
- **Database:** MySQL (Trilogy adapter) with SQLite (Solid Queue, Solid Cache, Solid Cable)
- **Background jobs:** Solid Queue (database-backed, no Redis)
- **HTTP client patterns:** Net::HTTP (web-push, webhook deliveries)
- **Module organization:** Rails concerns, namespaced models (Card::Eventable, Board::Publishable, etc.)

## Value Mapping

### Primary Value Action
**Creating and progressing cards through workflow columns** — A card moves from triage into a board column, gets worked on collaboratively, and eventually closes. If this drops to zero, the product has failed.

### Core Features (directly deliver value)
1. **Card management** — Create, edit, assign, comment on, and close cards. Cards are the atomic unit of work.
2. **Board & column workflow** — Boards organize cards into columns representing workflow stages. Moving cards through columns is how work progresses.
3. **Collaboration** — Comments, mentions, assignments, and reactions enable team coordination on cards.
4. **Search** — Full-text search across cards and comments (16-shard MySQL) lets users find work quickly.

### Supporting Features (enable core actions)
1. **Notifications** — Bundled email notifications, push notifications, and in-app notification tray keep users aware of changes.
2. **Entropy (auto-postpone)** — Automatically postpones stale cards to "not now" to prevent backlog bloat.
3. **Tags & filters** — Categorization and saved filters help users organize and find relevant cards.
4. **Board publication** — Public read-only board views with shareable keys for external stakeholders.
5. **Webhooks** — Slack, Campfire, and Basecamp integrations for card lifecycle events.
6. **Import/export** — Data portability between Fizzy instances (OSS and SaaS).
7. **Pins & golden cards** — Users can pin important cards and mark cards as "golden" (high priority).
8. **Steps (checklists)** — Sub-tasks within a card for breaking down work.

## Entity Model

### Users
- **ID format:** UUID (UUIDv7, base36-encoded, 25-char strings)
- **Roles:** owner, admin, member, system
- **Multi-account:** yes — an Identity (email) can have Users in multiple Accounts

### Accounts
- **ID format:** UUID internally; `external_account_id` (7+ digit integer) used in URLs
- **Hierarchy:** flat (no nested accounts)

## Group Hierarchy

```
Account
└── Board
```

| Group Type | Parent | Where Actions Happen |
|------------|--------|---------------------|
| Account | — | User management, settings, billing, entropy config |
| Board | Account | Card creation, column workflow, collaboration, webhooks |

**Default event level:** Board — most user actions (card operations, comments, assignments) occur within a board context.
**Admin actions at:** Account — user invitations, settings, billing, cancellation.

## Current State
- **Existing tracking:** Segment (analytics-ruby 2.5.0) with 35 live events across lifecycle, core value, collaboration, configuration, billing, and feature usage categories.
- **Documentation:** See `.telemetry/current-state.yaml`, `.telemetry/current-implementation.md`, and `.telemetry/audits/2026-03-11.md`.
- **Architecture:** Centralized `Analytics` class with constants, async delivery via `Analytics::DeliveryJob` (Solid Queue). Server-side only.

## Integration Targets
| Destination | Purpose | Priority |
|-------------|---------|----------|
| Segment | CDP — route events to downstream tools | Primary |

## Codebase Observations
- **Feature areas inferred:** From routes — boards, cards, columns, comments, assignments, tags, filters, notifications, search, webhooks, signups, sessions, exports/imports, public boards, QR codes, user management.
- **Entity model inferred:** From models — Account → Users, Boards → Columns → Cards → Comments/Assignments/Steps/Tags/Reactions. Identity → Users (multi-account). Events track all significant actions with polymorphic associations.
- **Internal event system:** The `Eventable` concern and `Event` model already track domain actions (card_published, card_closed, card_assigned, comment_created, etc.) for activity feeds and webhook dispatch. This is an excellent foundation — analytics events can mirror these domain events.

Tracking Plan

File: .telemetry/tracking-plan.yamlGenerated by the Design skill

The complete tracking plan — entities, groups, events, properties, and implementation notes in a structured format.

.telemetry/tracking-plan.yaml
# Tracking Plan: Fizzy
# A collaborative kanban-style issue tracker by 37signals/Basecamp

meta:
  product: "Fizzy"
  version: 1
  created: 2026-03-11
  updated: 2026-03-11
  owner: "team"
  destinations:
    - segment
  naming_convention: "object.action (snake_case)"
  pii_policy: traits_only
  internal_user_policy: by_role  # Exclude system role users from analytics

# -----------------------------------------
# Entities: Users
# -----------------------------------------

entities:
  user:
    id_property: user_id
    id_format: "UUID (UUIDv7, base36-encoded, 25-char)"
    traits:
      - name: email
        type: string
        pii: true
        required: true
        update_pattern: on_change
      - name: name
        type: string
        pii: true
        required: true
        update_pattern: on_change
      - name: role
        type: string
        enum: [owner, admin, member]
        required: true
        update_pattern: on_change
      - name: created_at
        type: datetime
        required: true
        update_pattern: one_time
      - name: account_id
        type: string
        required: true
        update_pattern: one_time
      - name: is_verified
        type: boolean
        required: true
        update_pattern: on_change

# -----------------------------------------
# Groups: Account (single level)
# -----------------------------------------

groups:
  - type: account
    id_format: "UUID (external_account_id used in URLs)"
    is_top_level: true
    traits:
      - name: name
        type: string
        required: true
        update_pattern: on_change
      - name: plan
        type: string
        enum: [free, unlimited, unlimited_extra_storage]
        required: true
        update_pattern: on_change
      - name: created_at
        type: datetime
        required: true
        update_pattern: one_time
      - name: user_count
        type: integer
        required: false
        update_pattern: scheduled
      - name: board_count
        type: integer
        required: false
        update_pattern: scheduled
      - name: card_count
        type: integer
        required: false
        update_pattern: scheduled
      - name: active_user_count_30d
        type: integer
        required: false
        update_pattern: scheduled
      - name: storage_used_bytes
        type: integer
        required: false
        update_pattern: scheduled
      - name: entropy_period_days
        type: integer
        required: false
        update_pattern: on_change
      - name: mrr
        type: number
        required: false
        update_pattern: scheduled
        description: "Monthly recurring revenue in cents"
      - name: has_join_code
        type: boolean
        required: false
        update_pattern: on_change
      - name: webhook_count
        type: integer
        required: false
        update_pattern: scheduled
    group_call_triggers:
      - trigger: creation
        description: "When a new account is created during signup"
      - trigger: trait_change
        description: "When plan changes, name changes, entropy settings change"
      - trigger: scheduled_sync
        description: "Daily snapshot of user_count, board_count, card_count, active_user_count_30d, storage_used_bytes, mrr"

# -----------------------------------------
# Events
# -----------------------------------------

events:

  # ===== LIFECYCLE =====

  - name: account.created
    category: lifecycle
    group_level: account
    description: "New Fizzy account is created via signup"
    properties:
      - name: account_name
        type: string
        required: true
    expected_frequency: low

  - name: user.signed_up
    category: lifecycle
    group_level: account
    description: "User creates an identity and begins signup flow"
    properties:
      - name: signup_source
        type: string
        enum: [organic, magic_link, join_code]
        required: true
    expected_frequency: low

  - name: user.signed_in
    category: lifecycle
    group_level: account
    description: "User signs in via magic link"
    properties:
      - name: method
        type: string
        enum: [magic_link, access_token, session_transfer]
        required: true
    expected_frequency: high

  - name: user.joined
    category: lifecycle
    group_level: account
    description: "User joins an existing account via join code or invitation"
    properties:
      - name: join_method
        type: string
        enum: [join_code, magic_link]
        required: true
      - name: role
        type: string
        enum: [owner, admin, member]
        required: true
    expected_frequency: low

  # ===== CORE VALUE: BOARDS =====

  - name: board.created
    category: core_value
    group_level: account
    description: "User creates a new board"
    properties:
      - name: board_id
        type: string
        required: true
      - name: is_all_access
        type: boolean
        required: true
    expected_frequency: low

  # ===== CORE VALUE: CARDS (Primary Value Action) =====

  - name: card.created
    category: core_value
    group_level: account
    description: "User creates a new card on a board"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: has_description
        type: boolean
        required: false
      - name: has_image
        type: boolean
        required: false
    expected_frequency: high

  - name: card.triaged
    category: core_value
    group_level: account
    description: "Card is moved from triage into a board column"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: column_name
        type: string
        required: true
    expected_frequency: high

  - name: card.moved
    category: core_value
    group_level: account
    description: "Card is moved between columns within a board"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: from_column
        type: string
        required: true
      - name: to_column
        type: string
        required: true
    expected_frequency: high

  - name: card.closed
    category: core_value
    group_level: account
    description: "Card is closed (work completed)"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: medium

  - name: card.reopened
    category: core_value
    group_level: account
    description: "Closed card is reopened"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: low

  - name: card.postponed
    category: core_value
    group_level: account
    description: "Card is manually postponed to Not Now"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: medium

  - name: card.auto_postponed
    category: core_value
    group_level: account
    description: "Card is automatically postponed by entropy system due to inactivity"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: medium

  - name: card.board_changed
    category: core_value
    group_level: account
    description: "Card is moved to a different board"
    properties:
      - name: card_id
        type: string
        required: true
      - name: from_board_id
        type: string
        required: true
      - name: to_board_id
        type: string
        required: true
    expected_frequency: low

  - name: card.assigned
    category: core_value
    group_level: account
    description: "User is assigned to a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: assignee_id
        type: string
        required: true
      - name: is_self_assignment
        type: boolean
        required: true
    expected_frequency: medium

  - name: card.unassigned
    category: core_value
    group_level: account
    description: "User is unassigned from a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: assignee_id
        type: string
        required: true
    expected_frequency: low

  - name: card.gilded
    category: core_value
    group_level: account
    description: "Card is marked as golden (high priority)"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: low

  # ===== COLLABORATION =====

  - name: comment.created
    category: collaboration
    group_level: account
    description: "User adds a comment to a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: has_mention
        type: boolean
        required: false
      - name: has_attachment
        type: boolean
        required: false
    expected_frequency: high

  - name: reaction.added
    category: collaboration
    group_level: account
    description: "User adds a reaction to a card or comment"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
      - name: target_type
        type: string
        enum: [card, comment]
        required: true
    expected_frequency: medium

  - name: user.deactivated
    category: collaboration
    group_level: account
    description: "User is deactivated from the account"
    properties:
      - name: deactivated_user_id
        type: string
        required: true
    expected_frequency: low

  - name: user.role_changed
    category: collaboration
    group_level: account
    description: "User's role is changed within the account"
    properties:
      - name: target_user_id
        type: string
        required: true
      - name: from_role
        type: string
        enum: [owner, admin, member]
        required: true
      - name: to_role
        type: string
        enum: [owner, admin, member]
        required: true
    expected_frequency: low

  # ===== CONFIGURATION =====

  - name: webhook.created
    category: configuration
    group_level: account
    description: "User creates a webhook on a board"
    properties:
      - name: board_id
        type: string
        required: true
      - name: webhook_type
        type: string
        enum: [slack, campfire, basecamp, custom]
        required: true
      - name: subscribed_action_count
        type: integer
        required: true
    expected_frequency: low

  - name: board.published
    category: configuration
    group_level: account
    description: "Board is published with a public shareable link"
    properties:
      - name: board_id
        type: string
        required: true
    expected_frequency: low

  - name: board.unpublished
    category: configuration
    group_level: account
    description: "Board's public link is removed"
    properties:
      - name: board_id
        type: string
        required: true
    expected_frequency: low

  - name: entropy.configured
    category: configuration
    group_level: account
    description: "Entropy (auto-postpone) settings changed at account or board level"
    properties:
      - name: level
        type: string
        enum: [account, board]
        required: true
      - name: board_id
        type: string
        required: false
        description: "Present when level is board"
      - name: period_days
        type: integer
        required: true
    expected_frequency: low

  - name: notification.settings_changed
    category: configuration
    group_level: account
    description: "User changes their notification preferences"
    properties: []
    expected_frequency: low

  # ===== BILLING =====

  - name: plan.upgraded
    category: billing
    group_level: account
    description: "Account upgrades to a higher plan"
    properties:
      - name: from_plan
        type: string
        enum: [free, unlimited, unlimited_extra_storage]
        required: true
      - name: to_plan
        type: string
        enum: [free, unlimited, unlimited_extra_storage]
        required: true
    expected_frequency: low

  - name: plan.downgraded
    category: billing
    group_level: account
    description: "Account downgrades to a lower plan"
    properties:
      - name: from_plan
        type: string
        enum: [free, unlimited, unlimited_extra_storage]
        required: true
      - name: to_plan
        type: string
        enum: [free, unlimited, unlimited_extra_storage]
        required: true
    expected_frequency: low

  - name: limit.reached
    category: billing
    group_level: account
    description: "Account hits a plan limit (card count or storage)"
    properties:
      - name: limit_type
        type: string
        enum: [cards, storage]
        required: true
      - name: current_usage
        type: integer
        required: true
      - name: limit_value
        type: integer
        required: true
    expected_frequency: low

  - name: account.cancelled
    category: billing
    group_level: account
    description: "Account is cancelled"
    properties: []
    expected_frequency: low

  # ===== FEATURE USAGE =====

  - name: search.performed
    category: feature_usage
    group_level: account
    description: "User performs a search"
    properties:
      - name: results_count
        type: integer
        required: true
      - name: query_length
        type: integer
        required: true
        description: "Length of query string, not the query itself"
    expected_frequency: medium

  - name: filter.created
    category: feature_usage
    group_level: account
    description: "User creates a saved filter"
    properties:
      - name: board_id
        type: string
        required: false
    expected_frequency: low

  - name: export.created
    category: feature_usage
    group_level: account
    description: "User initiates an account data export"
    properties: []
    expected_frequency: low

  - name: import.created
    category: feature_usage
    group_level: account
    description: "User initiates an account data import"
    properties: []
    expected_frequency: low

  - name: step.completed
    category: feature_usage
    group_level: account
    description: "User completes a step (checklist item) on a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: medium

  - name: card.pinned
    category: feature_usage
    group_level: account
    description: "User pins a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: low

  - name: card.tagged
    category: feature_usage
    group_level: account
    description: "User adds a tag to a card"
    properties:
      - name: card_id
        type: string
        required: true
      - name: board_id
        type: string
        required: true
    expected_frequency: medium

# -----------------------------------------
# Snapshot Sync
# -----------------------------------------

snapshot_sync:
  cadence: daily

  traits:
    - entity: account
      trait: user_count
      type: integer
      source: "COUNT of active users per account"

    - entity: account
      trait: board_count
      type: integer
      source: "COUNT of boards per account"

    - entity: account
      trait: card_count
      type: integer
      source: "accounts.cards_count column"

    - entity: account
      trait: active_user_count_30d
      type: integer
      source: "COUNT of users with events in last 30 days"

    - entity: account
      trait: storage_used_bytes
      type: integer
      source: "SUM of active storage blob byte_size per account"

    - entity: account
      trait: mrr
      type: number
      source: "Current plan price from billing system (Stripe via Queenbee)"

    - entity: account
      trait: webhook_count
      type: integer
      source: "COUNT of active webhooks per account"

# -----------------------------------------
# Implementation Notes
# -----------------------------------------

implementation_notes:
  - "All events include user_id (via Segment identify) and account context (via Segment group)"
  - "Use ISO 8601 for all datetime properties"
  - "IDs are UUIDs (UUIDv7, base36-encoded, 25-char strings) — pass as-is, no prefix needed"
  - "Never include PII in event properties — email and name are in identify() traits only"
  - "Exclude system role users from all tracking calls (internal_user_policy: by_role)"
  - "Server-side tracking via analytics-ruby gem — userId required on every call"
  - "group() call with account context on every sign-in and on account/trait changes"
  - "Segment group() uses account UUID as groupId — all events attributed to account level"
  - "Card content (titles, descriptions, comments) must never appear in event properties"
  - "Snapshot sync runs as a daily Solid Queue job, sending group() calls with updated traits"

Delta

File: .telemetry/delta.mdGenerated by the Design skill

The gap analysis between current state and the target tracking plan — what needs to change and in what order.

.telemetry/delta.md
# Delta: Current → Target

**Current state:** Greenfield — no analytics tracking exists in the codebase.
**Target:** 30 events across 6 categories, tracked via Segment (server-side, `analytics-ruby`).

## Add (all events — nothing exists today)

### Lifecycle (4 events)
| Event | Why |
|-------|-----|
| `account.created` | Track new account creation for acquisition funnel |
| `user.signed_up` | Track identity creation — top of the user funnel |
| `user.signed_in` | Session frequency and authentication method usage |
| `user.joined` | Track team growth — users joining via join codes or invitations |

### Core Value (12 events)
| Event | Why |
|-------|-----|
| `board.created` | Board creation is a prerequisite for all work — adoption signal |
| `card.created` | Primary value action — creating work items |
| `card.triaged` | Moving cards from triage into columns — workflow engagement |
| `card.moved` | Card progression through workflow columns — depth of use |
| `card.closed` | Work completion — the ultimate value delivery signal |
| `card.reopened` | Reopening indicates quality or scope changes |
| `card.postponed` | Manual postponement — backlog management signal |
| `card.auto_postponed` | Entropy system engagement — measures backlog hygiene |
| `card.board_changed` | Cross-board card movement — organizational complexity signal |
| `card.assigned` | Assignment is core collaboration — who is doing the work |
| `card.unassigned` | Unassignment tracks workload redistribution |
| `card.gilded` | Golden cards indicate prioritization depth |

### Collaboration (4 events)
| Event | Why |
|-------|-----|
| `comment.created` | Comments are the primary collaboration mechanism |
| `reaction.added` | Lightweight engagement signal — team dynamics |
| `user.deactivated` | Team churn at the user level |
| `user.role_changed` | Role changes indicate organizational maturity |

### Configuration (5 events)
| Event | Why |
|-------|-----|
| `webhook.created` | Integration setup — stickiness signal |
| `board.published` | Public board usage — external sharing |
| `board.unpublished` | Removing public access — lifecycle |
| `entropy.configured` | Entropy tuning indicates sophisticated usage |
| `notification.settings_changed` | Notification preference changes — engagement config |

### Billing (4 events)
| Event | Why |
|-------|-----|
| `plan.upgraded` | Revenue expansion signal |
| `plan.downgraded` | Churn risk signal |
| `limit.reached` | Conversion trigger — free plan hitting card/storage limits |
| `account.cancelled` | Account churn — the most critical negative signal |

### Feature Usage (6 events)
| Event | Why |
|-------|-----|
| `search.performed` | Search usage correlates with content volume and engagement |
| `filter.created` | Saved filters indicate power usage |
| `export.created` | Data export usage — may indicate churn risk or compliance |
| `import.created` | Migration activity — new customer onboarding |
| `step.completed` | Checklist completion measures depth of card usage |
| `card.pinned` | Pinning shows personal workflow customization |
| `card.tagged` | Tagging shows organizational sophistication |

## Identity Management (new)

| Call | When | Traits |
|------|------|--------|
| `identify()` | Sign-in, signup, trait changes | email, name, role, created_at, account_id, is_verified |
| `group()` | Sign-in, account creation, plan changes, daily snapshot | name, plan, created_at, user_count, board_count, card_count, active_user_count_30d, storage_used_bytes, mrr, entropy_period_days, has_join_code, webhook_count |

## Snapshot Sync (new)

Daily Solid Queue job sends `group()` calls with current account metrics:
- `user_count`, `board_count`, `card_count`
- `active_user_count_30d`
- `storage_used_bytes`
- `mrr`
- `webhook_count`

## Summary

| Category | Event Count |
|----------|-------------|
| Lifecycle | 4 |
| Core Value | 12 |
| Collaboration | 4 |
| Configuration | 5 |
| Billing | 4 |
| Feature Usage | 7 |
| **Total** | **36** |

All 36 events are **ADD** — greenfield, nothing to rename, keep, or remove.

## Implementation Priority

**Phase 1 — Foundation (do first):**
- Segment SDK setup (`analytics-ruby`)
- `identify()` and `group()` on sign-in
- `account.created`, `user.signed_up`, `user.signed_in`, `user.joined`

**Phase 2 — Core Value (do next):**
- All card lifecycle events: `card.created`, `card.triaged`, `card.moved`, `card.closed`, `card.reopened`, `card.postponed`, `card.auto_postponed`
- `board.created`, `card.assigned`, `card.unassigned`

**Phase 3 — Collaboration & Billing:**
- `comment.created`, `reaction.added`
- `plan.upgraded`, `plan.downgraded`, `limit.reached`, `account.cancelled`

**Phase 4 — Configuration & Feature Usage:**
- Remaining events: webhooks, board publishing, entropy, search, filters, exports, imports, steps, pins, tags
- Daily snapshot sync job

## Next Step

Run the **product-tracking-generate-implementation-guide** skill to translate this plan into Segment `analytics-ruby` instrumentation code (e.g., *"create instrumentation guide"* or *"generate SDK guide"*).

Instrumentation Guide

File: .telemetry/instrument.mdGenerated by the Instrument skill

SDK setup, identity management, every event with template code, the complete tracking module, architecture, and rollout strategy.

.telemetry/instrument.md
# Instrumentation Guide

## Target: Segment (server-side Ruby via `analytics-ruby`)

Generated from tracking-plan.yaml v1 on 2026-03-11.

## SDK Setup

### Dependencies

Add to `Gemfile`:

```ruby
gem "analytics-ruby", "~> 2.4", require: "segment/analytics"
```

Then run `bundle install`.

### Initialization

Create an initializer:

```ruby
# config/initializers/segment.rb
Rails.application.config.after_initialize do
  $segment = Segment::Analytics.new(
    write_key: Rails.application.credentials.segment[:write_key],
    on_error: ->(status, msg) { Rails.logger.error("[Segment] #{status}: #{msg}") }
  )

  at_exit { $segment.flush }
end
```

### Environment Variables

| Variable | Purpose | Required |
|----------|---------|----------|
| `credentials.segment.write_key` | Segment source write key | Yes |
| `credentials.segment.write_key` (staging) | Separate Segment source for non-production | Recommended |

Use Rails credentials (`bin/rails credentials:edit`) to store the write key. Use a separate Segment source for staging/development to avoid polluting production data.

---

## Identity

### identify()

**Syntax (analytics-ruby):**

```ruby
$segment.identify(
  user_id: "user-uuid-here",
  traits: { ... }
)
```

**User Traits:**

| Trait | Type | PII | When Updated |
|-------|------|-----|-------------|
| `email` | string | yes | On change |
| `name` | string | yes | On change |
| `role` | string | no | On change |
| `created_at` | datetime | no | Once (signup) |
| `account_id` | string | no | Once (signup) |
| `is_verified` | boolean | no | On change |

**When to Call:**
- After successful magic link sign-in (session creation)
- After signup completion
- When user traits change (name, role, email, verification)

**Template Code:**

```ruby
$segment.identify(
  user_id: user.id,
  traits: {
    email: user.identity.email_address,
    name: user.name,
    role: user.role,
    created_at: user.created_at.iso8601,
    account_id: user.account_id,
    is_verified: user.verified?
  }
)
```

### group()

**Syntax (analytics-ruby):**

```ruby
$segment.group(
  user_id: "user-uuid-here",
  group_id: "account-uuid-here",
  traits: { ... }
)
```

**Group Hierarchy:**

| Level | SDK Mapping | ID Source | Parent |
|-------|-------------|-----------|--------|
| Account | `group_id` on `group()` call | `account.id` (UUID) | None (top level) |

Fizzy uses a single group level (Account). All events are attributed to the account level via `context: { group_id: account.id }` on track calls.

**Group Traits:**

| Trait | Type | When Updated |
|-------|------|-------------|
| `name` | string | On change |
| `plan` | string (enum: free, unlimited, unlimited_extra_storage) | On change |
| `created_at` | datetime | Once |
| `user_count` | integer | Daily snapshot |
| `board_count` | integer | Daily snapshot |
| `card_count` | integer | Daily snapshot |
| `active_user_count_30d` | integer | Daily snapshot |
| `storage_used_bytes` | integer | Daily snapshot |
| `entropy_period_days` | integer | On change |
| `mrr` | number | Daily snapshot |
| `has_join_code` | boolean | On change |
| `webhook_count` | integer | Daily snapshot |

**When to Call:**
- After sign-in (associate user with account, refresh traits)
- After account creation (initial traits)
- When plan changes (upgrade/downgrade)
- When account settings change (name, entropy)
- Daily snapshot sync (usage metrics: user_count, board_count, card_count, etc.)

**Template Code:**

```ruby
$segment.group(
  user_id: user.id,
  group_id: account.id,
  traits: {
    name: account.name,
    plan: account_plan_name(account),
    created_at: account.created_at.iso8601,
    user_count: account.users.where(active: true).count,
    board_count: account.boards.count,
    card_count: account.cards_count,
    has_join_code: account.join_code.present?,
    entropy_period_days: account.entropy_period&.in_days
  }
)
```

---

## Events

### track()

**Syntax (analytics-ruby):**

```ruby
$segment.track(
  user_id: "user-uuid-here",
  event: "object.action",
  properties: { ... },
  context: { group_id: "account-uuid-here" }
)
```

**SDK Constraints:**
- Server-side is stateless — `user_id` is required on every call
- `context: { group_id: ... }` must be included on every track call for account attribution
- Properties are fully supported (Segment stores and forwards them to all destinations)

**Template Code:**

```ruby
# Core value event with properties
$segment.track(
  user_id: Current.user.id,
  event: "card.created",
  properties: {
    card_id: card.id,
    board_id: card.board_id,
    has_description: card.description.present?,
    has_image: card.image.attached?
  },
  context: { group_id: Current.account.id }
)

# Simple lifecycle event
$segment.track(
  user_id: Current.user.id,
  event: "user.signed_in",
  properties: {
    method: "magic_link"
  },
  context: { group_id: Current.account.id }
)
```

### Group-Level Attribution

All Fizzy events are attributed to the **account** level. Include `context: { group_id: account.id }` on every track call:

```ruby
# Every track call follows this pattern
$segment.track(
  user_id: Current.user.id,
  event: "card.closed",
  properties: { card_id: card.id, board_id: card.board_id },
  context: { group_id: Current.account.id }
)
```

Downstream tools (Accoil, Amplitude, Mixpanel) use this `group_id` to attribute the event to the correct account for engagement scoring and account-level analysis.

---

## Complete Tracking Module

This is the full, copy-paste-ready tracking module. It follows Fizzy's conventions: Rails concerns, Solid Queue jobs, and the `Current` pattern for request context.

```ruby
# app/models/analytics.rb
#
# Centralized analytics tracking via Segment.
# Usage:
#   Analytics.identify(user)
#   Analytics.group(user, account)
#   Analytics.track(user, "card.created", { card_id: card.id, board_id: card.board_id })
#
class Analytics
  EXCLUDED_ROLES = %w[system].freeze

  class << self
    def identify(user)
      return if excluded?(user)

      deliver(:identify,
        user_id: user.id,
        traits: user_traits(user)
      )
    end

    def group(user, account)
      return if excluded?(user)

      deliver(:group,
        user_id: user.id,
        group_id: account.id,
        traits: account_traits(account)
      )
    end

    def track(user, event, properties = {})
      return if excluded?(user)

      deliver(:track,
        user_id: user.id,
        event: event,
        properties: properties,
        context: { group_id: user.account_id }
      )
    end

    def identify_and_group(user)
      identify(user)
      group(user, user.account)
    end

    def snapshot_account(account)
      deliver(:group,
        user_id: account.system_user.id,
        group_id: account.id,
        traits: snapshot_traits(account)
      )
    end

    private
      def excluded?(user)
        EXCLUDED_ROLES.include?(user.role)
      end

      def deliver(method, payload)
        Analytics::DeliveryJob.perform_later(method.to_s, payload)
      end

      def user_traits(user)
        {
          email: user.identity&.email_address,
          name: user.name,
          role: user.role,
          created_at: user.created_at.iso8601,
          account_id: user.account_id,
          is_verified: user.verified?
        }
      end

      def account_traits(account)
        {
          name: account.name,
          plan: plan_name(account),
          created_at: account.created_at.iso8601,
          has_join_code: account.join_code.present?,
          entropy_period_days: account.entropies.find_by(board: nil)&.period
        }
      end

      def snapshot_traits(account)
        {
          name: account.name,
          plan: plan_name(account),
          created_at: account.created_at.iso8601,
          user_count: account.users.where(active: true).count,
          board_count: account.boards.count,
          card_count: account.cards_count,
          active_user_count_30d: account.users.where("last_seen_at > ?", 30.days.ago).count,
          storage_used_bytes: account_storage_bytes(account),
          mrr: account_mrr(account),
          webhook_count: account.webhooks.active.count,
          has_join_code: account.join_code.present?
        }
      end

      def plan_name(account)
        if account.respond_to?(:subscription) && account.subscription
          account.subscription.plan_name
        else
          "free"
        end
      end

      def account_mrr(account)
        if account.respond_to?(:subscription) && account.subscription
          account.subscription.price * 100  # cents
        else
          0
        end
      end

      def account_storage_bytes(account)
        ActiveStorage::Blob
          .joins(:attachments)
          .where(active_storage_attachments: { record_type: account_record_types })
          .sum(:byte_size)
      end

      def account_record_types
        %w[Card Comment Board User]
      end
  end
end
```

```ruby
# app/jobs/analytics/delivery_job.rb
#
# Single delivery job for all Segment calls (identify, group, track).
# Runs via Solid Queue. Automatically retries on failure.
#
class Analytics::DeliveryJob < ApplicationJob
  queue_as :default
  retry_on StandardError, wait: :polynomially_longer, attempts: 3

  def perform(method, payload)
    $segment.public_send(method, **payload.symbolize_keys)
  end
end
```

```ruby
# app/jobs/analytics/snapshot_job.rb
#
# Daily snapshot sync — sends current account metrics as group traits.
# Add to config/recurring.yml:
#
#   analytics_snapshot:
#     class: Analytics::SnapshotJob
#     schedule: every day at 03:00
#
class Analytics::SnapshotJob < ApplicationJob
  queue_as :default

  def perform
    Account.active.find_each do |account|
      Analytics.snapshot_account(account)
    end
  end
end
```

---

## Architecture

### Client vs Server

All tracking is **server-side only** via `analytics-ruby`. This keeps the Segment write key out of client-side code, ensures every event has reliable user and account context from the Rails session, and avoids ad-blocker interference.

### Queues and Batching

- **Application layer:** Every analytics call is enqueued via `Analytics::DeliveryJob` (Solid Queue). This makes tracking non-blocking — controller actions return immediately.
- **SDK layer:** `analytics-ruby` batches calls internally and flushes periodically. The `at_exit { $segment.flush }` in the initializer ensures delivery on shutdown.

### Shutdown / Flush

Handled in `config/initializers/segment.rb`:

```ruby
at_exit { $segment.flush }
```

This ensures any buffered events are delivered when the Rails process exits (deploy, restart).

### Error Handling

- **SDK errors:** The `on_error` callback in the initializer logs to Rails logger. Segment API errors (rate limits, payload issues) surface here.
- **Job failures:** `Analytics::DeliveryJob` retries 3 times with polynomial backoff. After exhaustion, Solid Queue marks the job as failed for investigation via Mission Control::Jobs.
- **Non-blocking:** Analytics never raises in request processing. The `perform_later` call is fire-and-forget from the controller's perspective.

---

## Verification

### Confirming Delivery

1. **Segment Debugger:** In Segment UI, go to Sources → [Your Source] → Debugger. Events appear in real-time (within seconds of delivery).
2. **Rails logs:** The `on_error` callback logs failures. In development, add verbose logging:

```ruby
# config/initializers/segment.rb (development only)
if Rails.env.development?
  $segment = Segment::Analytics.new(
    write_key: "dev_write_key",
    on_error: ->(status, msg) { Rails.logger.error("[Segment] #{status}: #{msg}") },
    stub: true  # logs calls without sending
  )
end
```

### Expected Latency

- **Application → Solid Queue:** Immediate (database insert)
- **Solid Queue → Segment API:** Seconds (job pickup + SDK batch flush)
- **Segment → Destinations:** Seconds to minutes (depends on destination)

### Success vs Failure

| Scenario | What You See |
|----------|-------------|
| Successful delivery | Event appears in Segment Debugger within seconds |
| Invalid write key | No error from API (Segment returns 200), events silently dropped. Check Segment UI → Settings |
| Rate limited | `on_error` callback fires with status 429. Job retries handle this |
| Payload too large | `on_error` with status 400. Check event properties size (max 32 KB) |

### Development Testing

- Use a **separate Segment source** for development/staging. Create a "Fizzy Dev" source in Segment and store its write key in development credentials.
- Use `stub: true` in the analytics-ruby initializer to log calls without sending to Segment.
- Run `Analytics::DeliveryJob.perform_now(...)` in rails console to test delivery synchronously.

---

## Rollout Strategy

### Phase 1: Foundation
1. Add `analytics-ruby` gem, create initializer
2. Create `Analytics` model and `Analytics::DeliveryJob`
3. Wire `identify()` and `group()` on sign-in
4. Track: `account.created`, `user.signed_up`, `user.signed_in`, `user.joined`
5. Verify in Segment Debugger

### Phase 2: Core Value
1. Track card lifecycle: `card.created`, `card.triaged`, `card.moved`, `card.closed`, `card.reopened`, `card.postponed`, `card.auto_postponed`
2. Track: `board.created`, `card.assigned`, `card.unassigned`, `card.board_changed`, `card.gilded`
3. Verify event volume and property shapes in Segment

### Phase 3: Collaboration & Billing
1. Track: `comment.created`, `reaction.added`, `user.deactivated`, `user.role_changed`
2. Track: `plan.upgraded`, `plan.downgraded`, `limit.reached`, `account.cancelled`
3. Connect downstream destinations (Accoil, Amplitude, etc.)

### Phase 4: Feature Usage & Snapshots
1. Track: `search.performed`, `filter.created`, `export.created`, `import.created`, `step.completed`, `card.pinned`, `card.tagged`
2. Track: `webhook.created`, `board.published`, `board.unpublished`, `entropy.configured`, `notification.settings_changed`
3. Deploy `Analytics::SnapshotJob` with daily schedule
4. Verify snapshot traits appear on accounts in downstream tools

---

## SDK-Specific Constraints

- **Server-side is stateless:** `user_id` must be on every call — there is no session state in the SDK
- **group() does not persist:** Calling `group()` does not automatically attach group context to subsequent `track()` calls. Include `context: { group_id: ... }` on every track call explicitly
- **Flush on exit:** Without `at_exit { $segment.flush }`, buffered events may be lost during deploys
- **Rate limit:** 1,000 requests/sec per Segment workspace. Unlikely to hit with job-based delivery, but monitor during snapshot sync of large account counts
- **Max event size:** 32 KB per event. Card titles/descriptions must never be included in properties
- **Regional routing:** If using Segment EU workspace, configure the EU host (`https://eu1.api.segmentapis.com`) in the SDK initializer

## Coverage Gaps

- **analytics-ruby v2.x:** The reference file documents Node.js and HTTP API patterns. The Ruby gem follows the same API shape but with Ruby keyword arguments. The module above uses verified `analytics-ruby` syntax.
- **Solid Queue recurring jobs:** The snapshot job uses `config/recurring.yml` syntax per Fizzy's existing conventions. Verify the schedule entry works in your Solid Queue version.
- **SaaS billing integration:** The `plan_name` and `account_mrr` helper methods assume the SaaS engine exposes `subscription.plan_name` and `subscription.price`. Adjust to match the actual Queenbee/Stripe integration interface.

Audit

File: .telemetry/audits/2026-03-11.mdGenerated by the Audit skill

A point-in-time snapshot of every analytics call in the codebase — event names, locations, properties, identity management, and hygiene observations.

This skill was called after the initial implementation, to demonstrate the output from this step.

.telemetry/audits/2026-03-11.md
# Tracking Audit: 2026-03-11

**Codebase:** Fizzy (collaborative project management / kanban)
**SDK:** Segment (analytics-ruby 2.5.0)

## Current Tracking Inventory

| # | Event Name | Category (inferred) | Location(s) | Properties |
|---|-----------|---------------------|-------------|------------|
| 1 | `account.created` | lifecycle | `signups/completions_controller.rb:51` | account_name |
| 2 | `user.signed_up` | lifecycle | `signups/completions_controller.rb:52` | signup_source |
| 3 | `user.signed_in` | lifecycle | `sessions/magic_links_controller.rb:94` | method |
| 4 | `user.joined` | lifecycle | `join_codes_controller.rb:20` | join_method, role |
| 5 | `board.created` | core_value | `boards_controller.rb:26` | board_id, is_all_access |
| 6 | `card.created` | core_value | `card/statuses.rb:31` | card_id, board_id, has_description, has_image |
| 7 | `card.triaged` | core_value | `card/triageable.rb:28` | card_id, board_id, column_name |
| 8 | `card.closed` | core_value | `card/closeable.rb:37` | card_id, board_id |
| 9 | `card.reopened` | core_value | `card/closeable.rb:47` | card_id, board_id |
| 10 | `card.postponed` | core_value | `card/postponable.rb:41` | card_id, board_id |
| 11 | `card.auto_postponed` | core_value | `card/postponable.rb:41` | card_id, board_id |
| 12 | `card.board_changed` | core_value | `card.rb:91` | card_id, from_board_id, to_board_id |
| 13 | `card.assigned` | core_value | `card/assignable.rb:34` | card_id, board_id, assignee_id, is_self_assignment |
| 14 | `card.unassigned` | core_value | `card/assignable.rb:49` | card_id, board_id, assignee_id |
| 15 | `card.gilded` | core_value | `cards/goldnesses_controller.rb:6` | card_id, board_id |
| 16 | `comment.created` | collaboration | `cards/comments_controller.rb:14` | card_id, board_id |
| 17 | `reaction.added` | collaboration | `cards/reactions_controller.rb:21` | card_id, board_id |
| 18 | `user.deactivated` | collaboration | `users_controller.rb:31` | deactivated_user_id |
| 19 | `user.role_changed` | collaboration | `users/roles_controller.rb:8` | changed_user_id, old_role, new_role |
| 20 | `webhook.created` | configuration | `webhooks_controller.rb:20` | board_id, webhook_id |
| 21 | `board.published` | configuration | `boards/publications_controller.rb:8` | board_id |
| 22 | `board.unpublished` | configuration | `boards/publications_controller.rb:18` | board_id |
| 23 | `entropy.configured` | configuration | `boards/entropies_controller.rb:8`, `account/entropies_controller.rb:6` | scope, board_id, auto_postpone_period |
| 24 | `notification.settings_changed` | configuration | `notifications/settings_controller.rb:10` | bundle_email_frequency |
| 25 | `plan.upgraded` | billing | `saas/stripe/webhooks_controller.rb:105` | old_plan, new_plan |
| 26 | `plan.downgraded` | billing | `saas/stripe/webhooks_controller.rb:105` | old_plan, new_plan |
| 27 | `limit.reached` | billing | `saas/card/limited.rb:7` | card_limit_exceeded, storage_limit_exceeded |
| 28 | `account.cancelled` | billing | `account/cancellations_controller.rb:5` | _(none)_ |
| 29 | `search.performed` | feature_usage | `searches_controller.rb:14` | _(none)_ |
| 30 | `filter.created` | feature_usage | `filters_controller.rb:6` | _(none)_ |
| 31 | `export.created` | feature_usage | `account/exports_controller.rb:13` | _(none)_ |
| 32 | `import.created` | feature_usage | `account/imports_controller.rb:42` | _(none)_ |
| 33 | `step.completed` | feature_usage | `cards/steps_controller.rb:25` | card_id, board_id |
| 34 | `card.pinned` | feature_usage | `cards/pins_controller.rb:10` | card_id, board_id |
| 35 | `card.tagged` | feature_usage | `cards/taggings_controller.rb:12` | card_id, board_id |

**Total events defined:** 36 constants
**Total events LIVE:** 35
**Total events ORPHANED:** 1

### Orphaned Events

| Event Name | Defined At | Note |
|-----------|-----------|------|
| `card.moved` | `app/models/analytics.rb:16` | Constant `CARD_MOVED` defined but never used in any `Analytics.track` call |

### Orphaned Code

| Item | Location | Note |
|------|----------|------|
| `AnalyticsTrackable` concern | `app/models/concerns/analytics_trackable.rb` | Defined but never included in any class |

## Identity Management

| Call Type | Present | Location(s) | Details |
|-----------|---------|-------------|---------|
| identify() | YES | `signups/completions_controller.rb:49`, `join_codes_controller.rb:18`, `sessions/magic_links_controller.rb:93` | Traits: email, name, role, created_at, account_id, is_verified |
| group() | YES | `signups/completions_controller.rb:50`, `join_codes_controller.rb:19`, `sessions/magic_links_controller.rb:93` | Group type: account. Traits: name, plan, created_at, has_join_code, entropy_period_days |
| snapshot_account() | YES | `analytics/snapshot_job.rb:6` (daily 03:00) | Enriched group traits: adds user_count, board_count, card_count, active_user_count_30d, storage_used_bytes, mrr, webhook_count |
| page()/screen() | NO | — | — |
| reset()/logout | NO | — | — |

## Observed Patterns

- **Naming style:** snake_case throughout, consistent
- **Naming format:** `object.action` (dot-separated), consistent across all 36 events
- **Centralization:** Fully centralized — all event names defined as constants on `Analytics` class, all calls route through `Analytics.track/identify/group` class methods
- **Async delivery:** All calls are non-blocking — enqueued via `Analytics::DeliveryJob` (Solid Queue background job with 3 retry attempts, polynomially increasing backoff)
- **Error handling:** SDK-level `on_error` logs to Rails logger; job-level retry; tracking failures never affect user requests
- **System user exclusion:** Users with `system` role are filtered out via `EXCLUDED_ROLES` before any call reaches Segment
- **Context enrichment:** All `track` calls include `context: { group_id: user.account_id }` to associate events with the account
- **Flush on shutdown:** `at_exit { $segment.flush }` registered in initializer

## Hygiene Notes

*Factual observations, not recommendations.*

1. `card.moved` is defined as `CARD_MOVED` constant but never called — no code tracks column-to-column movement within a board.
2. `AnalyticsTrackable` concern exists but is not included anywhere; all call sites use `Analytics.track` directly.
3. `entropy.configured` is tracked from two locations with different property shapes — board-level includes `board_id`, account-level does not.
4. `search.performed`, `filter.created`, `export.created`, and `import.created` are tracked with no properties beyond the implicit user and account context.
5. `account.cancelled` is tracked with no properties (no cancellation reason, account age, or plan).
6. The Segment write key has a hardcoded fallback value in the initializer 
7. `storage_bytes` in `snapshot_account` does not filter by account — it sums across all accounts.
8. Tracking calls in model concerns (card lifecycle) use `if Current.user` or `if user` guards — events from background jobs or system actions are silently skipped.
9. Plan change events (`plan.upgraded`/`plan.downgraded`) are tracked using the account owner as the user, not the user who initiated the change (Stripe webhook context).

Current Implementation

File: .telemetry/current-implementation.mdGenerated by the Audit skill

Implementation details discovered during the audit — how the SDK is wired up, delivery patterns, and architectural decisions.

.telemetry/current-implementation.md
## Current Implementation

**SDK:** Segment (analytics-ruby 2.5.0)
**Captured:** 2026-03-11

### Initialization

Segment is initialized in `config/initializers/segment.rb` using `Rails.application.config.after_initialize`. The client is assigned to a global variable `$segment`. Write key is resolved from Rails credentials (`segment.write_key`), then `ENV["SEGMENT_WRITE_KEY"]`, with a hardcoded fallback. In test environments, the SDK is stubbed (`stub: Rails.env.test?`).

### Client vs Server

All tracking calls are server-side only. No frontend/JavaScript analytics SDK is present.

### Call Routing

Centralized wrapper pattern via the `Analytics` class (`app/models/analytics.rb`):

1. **Event constants** — All event names are defined as constants on the `Analytics` class (e.g., `Analytics::CARD_CREATED = "card.created"`), organized into categories (Lifecycle, Core Value, Collaboration, Configuration, Billing, Feature Usage).

2. **Class methods**`Analytics.track(user, event, properties)`, `Analytics.identify(user)`, `Analytics.group(user, account)`, and `Analytics.identify_and_group(user)` are the public API. All methods filter out system-role users via `EXCLUDED_ROLES`.

3. **Async delivery** — All calls go through `Analytics::DeliveryJob` (a Solid Queue background job). The `deliver` private method enqueues the job with the Segment method name and payload. The job calls `$segment.public_send(method, **payload.symbolize_keys)`.

4. **Call sites** — Controllers call `Analytics.track` directly with the event constant and properties. Models also call `Analytics.track` directly (card lifecycle events live in model concerns).

5. **Unused concern** — An `AnalyticsTrackable` concern exists (`app/models/concerns/analytics_trackable.rb`) providing a `track_analytics` helper, but it is not included in any class.

### Identity Management

- **identify()** — Called at three entry points: signup completion, join code redemption, and sign-in (via `identify_and_group`). Traits: `email`, `name`, `role`, `created_at`, `account_id`, `is_verified`.

- **group()** — Called at the same three entry points. Group ID is the account UUID. Traits: `name`, `plan`, `created_at`, `has_join_code`, `entropy_period_days`.

- **snapshot_account()** — A separate method that sends enriched group traits for all active accounts daily via `Analytics::SnapshotJob` (recurring at 03:00). Adds: `user_count`, `board_count`, `card_count`, `active_user_count_30d`, `storage_used_bytes`, `mrr`, `webhook_count`.

- **Context** — All `track` calls include `context: { group_id: user.account_id }` to associate events with the account.

- **No reset/logout handling** — No analytics reset or cleanup call on session end.

### Environment Variables

- `SEGMENT_WRITE_KEY` — Segment write key (fallback from Rails credentials `segment.write_key`)
- Hardcoded fallback key present in initializer

### Error Handling

- **SDK errors** — Segment initializer registers an `on_error` callback that logs to `Rails.logger.error`.
- **Job retry**`Analytics::DeliveryJob` uses `retry_on StandardError, wait: :polynomially_longer, attempts: 3`.
- **Non-blocking** — All tracking is async via background jobs; tracking failures do not affect user-facing requests.

### Shutdown / Flush

Handled. `at_exit { $segment.flush }` is registered in the Segment initializer to flush pending events on process shutdown.

On this page