# Insights and analytics

> Understanding trackagoat's computed analytics metrics: engagement rates, posting velocity, average views, and period-over-period growth.

## What are derived metrics?

Raw metrics (views, likes, followers) tell you absolute counts. **Derived metrics** are computed ratios: rates and velocities that let you compare performance across creators and campaigns of different sizes.

All derived metrics are computed server-side from your historical snapshot data using timezone-aware day bucketing. Request them by adding a `metric=` parameter to any stats endpoint.

## Available metrics

| Metric ID | Formula | Entity support |
|-----------|---------|----------------|
| `engagement_rate_by_views` | `(likes + comments + shares + saves) / views × 100` | video, creator, campaign |
| `engagement_rate_by_followers` | `(likes + comments + shares + saves) / follower_count × 100` | creator, campaign |
| `posts_per_week` | `posts on that day × 7` | creator, campaign |
| `avg_views_per_post` | `views gained / videos posted through that day` | creator, campaign |
| `growth_rate_pop` | `(value[day] − value[day−1]) / value[day−1] × 100` | video, creator, campaign |

## Engagement rate by views (TikTok standard)

This is the primary TikTok engagement benchmark. It measures what percentage of viewers interacted with the content.

```text
engagement_rate = (likes + comments + shares + saves) / views × 100
```

A `null` value means views were 0 on that day (divide-by-zero). Days with baseline snapshots contribute 0 to the engagement numerator and denominator.

> **Tip: 2026 TikTok engagement benchmarks**
>
> - Under 1%: low
> - 1 to 3%: typical
> - 3 to 6%: strong
> - Above 6%: viral

```bash
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=<uuid>&metric=engagement_rate_by_views&from=2026-01-01&to=2026-03-31"
```

## Engagement rate by followers (IG-style)

Used when you want to normalize engagement against audience size rather than reach. Useful for creator comparisons regardless of how often they post.

```text
engagement_rate = (likes + comments + shares + saves) / follower_count × 100
```

The follower count used is the last known count as of each day (carry-forward). A `null` value means no follower data was available for that day.

This metric is **not supported for individual videos**: use `creator` or `campaign` entities.

```bash
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=<uuid>&metric=engagement_rate_by_followers&from=2026-01-01&to=2026-03-31"
```

## Posts per week

Posting velocity normalized to a weekly rate, computed on a per-day granularity basis.

```text
posts_per_week = posts_on_day × 7
```

A day with 0 posts returns `0.0` (a real rate of zero). `null` is returned only if there are no videos in scope at all.

```bash
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=<uuid>&metric=posts_per_week&from=2026-01-01&to=2026-03-31"
```

## Average views per post

Views gained in the period divided by the number of posts published through that day. Shows whether new posts are increasing or hurting per-video efficiency.

```text
avg_views_per_post = views_gained_on_day / videos_posted_through_day
```

`null` when no videos have been posted through that day yet.

```bash
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=<uuid>&metric=avg_views_per_post&from=2026-01-01&to=2026-03-31"
```

## Growth rate (period-over-period)

Day-over-day percentage change for any base metric. Pass `base_metric` to specify what to measure.

```text
growth_rate = (value[day] − value[day−1]) / value[day−1] × 100
```

- First point in range always returns `null` (no prior).
- `null` when the prior day's value was 0 or missing.
- Supported `base_metric` values: `views`, `likes`, `comments`, `shares`, `saves`, `followers` (creator only), `video_count` (creator only).

```bash
# Views growth rate for a creator
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=<uuid>&metric=growth_rate_pop&base_metric=views&from=2026-01-01&to=2026-03-31"

# Follower growth rate
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=<uuid>&metric=growth_rate_pop&base_metric=followers&from=2026-01-01&to=2026-03-31"
```

## Response shape

The `/api/v2/analytics` endpoint returns:

```json
{
  "data": {
    "series": [
      {
        "entity_ref": { "type": "creator", "id": "uuid", "label": "@handle" },
        "metric": "engagement_rate_by_views",
        "unit": "percent",
        "mode": "rate",
        "granularity": "day",
        "timezone": "America/New_York",
        "points": [
          { "bucket_start": "2026-01-01", "value": 4.2 },
          { "bucket_start": "2026-01-02", "value": null },
          { "bucket_start": "2026-01-03", "value": 3.8 }
        ]
      }
    ],
    "baselines": [
      {
        "entity_id": "video-uuid",
        "snapshot_at": "2026-01-01T08:00:00Z",
        "reason": "tracking_started"
      }
    ],
    "first_data_date": "2026-01-01",
    "tier_retention_days": 90
  },
  "error": null,
  "meta": { "cache_ttl_seconds": 300, "cached": false }
}
```

| Field | Description |
|-------|-------------|
| `series[].entity_ref` | Entity type, id, and display label for this series |
| `series[].points[].value` | Computed value, or `null` for divide-by-zero / missing data |
| `series[].timezone` | IANA timezone used for bucketing |
| `baselines` | Baseline marker events within the range: use as chart annotations |
| `first_data_date` | Earliest date with non-baseline data: use for "All time" preset |
| `tier_retention_days` | Your plan's retention window in days, or `null` for unlimited |

## Query parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `metric` | string | One of the metric IDs above. Omit for legacy response. |
| `granularity` | `day` \| `week` \| `month` \| `year` \| `cumulative` \| `custom` | Time bucket size. `custom` requires `bucket_days`. Defaults to `day`. |
| `tz` | IANA string | Timezone for bucketing. Defaults to your org's `analytics_tz`. |
| `formula` | `by_views` \| `by_followers` | Only for `engagement_rate_by_views` / `engagement_rate_by_followers`. |
| `base_metric` | string | Required for `growth_rate_pop`. |
| `from` | YYYY-MM-DD | Range start (default: 30 days ago). |
| `to` | YYYY-MM-DD | Range end (default: today). |

## Caching & freshness

All `/api/v2/analytics` responses are cached server-side for up to **5 minutes** (300 seconds). This means:

- The first request after cache expiry runs the full resolver and may take 1 to 2 seconds.
- Subsequent requests within the 5-minute window return in under 50ms.
- The `meta.cached` field in each JSON response is `true` when data came from cache and `false` on a cold run.
- The `meta.cache_ttl_seconds` field is always `300`.

### When is cache invalidated?

> **Note:**
>
> The cache is also tag-invalidated in real time on data mutations: not just when the TTL expires. Most changes take effect within seconds.

The cache is tag-invalidated (not just TTL-expired) in these situations:

| Event | Scope |
|-------|-------|
| Scraper writes new video stats | All analytics for the affected org |
| Scraper updates creator profile | All analytics for the affected org |
| Scraper rebuilds a campaign rollup | That campaign's analytics |
| You add/remove a video from a campaign | That campaign's analytics |
| You create, edit, or delete an annotation | Analytics for the annotated entity |
| You change the org's `analytics_tz` setting | All analytics for the org |

After a cache-busting event, the next request for affected data runs a fresh resolver. The **maximum staleness** in the worst case is 5 minutes (TTL alone), but in practice most data mutations trigger near-instant invalidation via the scraper webhook or inline `revalidateTag` calls.

### Partial-bucket staleness

Today's in-progress bucket may be up to **5 minutes stale** even with invalidation active: this is by design. The `partial_bucket` field tells you the `as_of` timestamp of the last data capture so you can show it to users.

---

## Campaign analytics

Campaign analytics data is served from a pre-aggregated rollup that is rebuilt nightly and updated within ~5 minutes whenever a video is added to or removed from a campaign. Today's partial data may be up to a few minutes behind as changes propagate through the invalidation queue.

### Campaign Insights trend charts

The **Campaign Insights** view opens with five trend charts built for recurring program reviews. Each follows the toolbar's date range and granularity and respects the campaign filter (all campaigns, or one).

| Chart | What a bucket shows |
|-------|---------------------|
| **Impressions Over Time** | Views gained during the bucket, counted on the day they happened rather than the day the video was posted. A video's pre-tracking views (its first snapshot) are credited to its posting day. |
| **Posts Over Time** | Videos posted in the bucket, by posted-at date. |
| **Creators Posting** | Distinct creators who posted at least once in the bucket. |
| **Impressions per Post** | Impressions in the bucket ÷ videos posted in the bucket. Buckets with no posts are left blank rather than shown as zero. |
| **New Creators Posting** | Creators whose earliest tracked post in the current scope falls in the bucket. Each creator counts once. |

With **All campaigns** selected, the charts cover every active tracked video in the project. With a single campaign selected they cover that campaign's resolved videos, including ones whose scraping has since expired, so a finished campaign keeps its history.

The underlying series is available as `GET /api/v2/analytics/aggregate/campaign-timeseries` (see the [aggregate API reference](https://www.trackagoat.com/docs/api-reference/aggregate.md#get-aggregatecampaign-timeseries)).

Historical `saves` values in older campaign snapshots may read as 0: this is expected, as earlier snapshots did not record save counts.

## Insights controls

The Insights pages (**Creator Insights**, **Account Insights**, **Video Insights** and **Campaign Insights**) share one toolbar. It controls every widget on the page.

### Period

Pick **Last 7 days**, **Last 30 days**, **Last 90 days**, **This month** or **All time**, or choose start and end dates. The default is the last 30 days.

### Platform filter

A **Platform** control scopes every widget to one platform: **All platforms** (the default), **TikTok** or **Instagram**. On Campaign Insights it applies to the totals, the trend charts, the campaign leaderboard and the campaign comparison; **Campaign progress** always counts every platform, because a campaign's view target covers all of them, and **Member overlap** lists campaign membership, which has no platform.

The project-level aggregate endpoints accept the same filter as a query parameter, e.g.:

```bash
curl -H "Authorization: Bearer tga_<key>" \
  "https://www.trackagoat.com/api/v2/analytics/aggregate/video-leaderboard?project_id=<uuid>&platform=tiktok"
```

`platform` accepts `tiktok` or `instagram`; omit it to include every platform.

### Campaign filter

**Video Insights**, **Account Insights**, and **Campaign Insights** add a **Campaign** control next to the platform filter. Pick a campaign to scope every widget on the page to the videos that campaign resolves to (videos added directly, every video from its member creators, and anything reached through nested campaigns), or leave it on **All campaigns**.

- On **Video Insights** it scopes the summary strip, video leaderboard, engagement breakdown, posting heatmap, view distribution, breakout videos, and the over-posting warning.
- On **Account Insights** it narrows the account list to accounts with at least one video in the campaign and counts only that campaign's videos toward each account's period metrics. Follower deltas stay per-account (they are not attributable to a campaign).
- On **Campaign Insights** it pins the summary strip, trend charts, leaderboard, comparison chart, and progress list to the chosen campaign.

The selection lives in the URL (`?campaign=<uuid>`), so a filtered view can be bookmarked or shared. The control is hidden while a project has no campaigns. Creator Insights does not have a campaign filter yet.

The same scope is available to API and agent callers as an optional `campaign_id` query parameter on the `video-leaderboard`, `posting-heatmap`, `view-distribution`, `outliers`, `over-posting-risks`, `account-leaderboard`, `campaign-leaderboard`, and `campaign-timeseries` aggregates. The campaign must belong to `project_id`; a campaign from another project simply matches nothing.

### Granularity

Controls the width of each time bucket: **Auto**, **Day**, **Week**, **Month** or **Year**.

> **Note: More controls on a single item's Analytics tab**
>
> Cumulative and custom granularity, the New/Cumulative mode toggle, **vs prior period**, **Compare with…**, **Breakdown** and **Annotations** are not on the Insights pages. They are on the **Analytics** tab of a single creator, campaign or video (**Compare with…** and **Breakdown** on creators and campaigns only). See [Analytics tab controls](#analytics-tab-controls).

## Analytics tab controls

Open a creator, campaign or video and go to its **Analytics** tab. Its toolbar has more controls than the Insights pages.

### Date range presets

The Analytics tab has its own presets:

| Preset | Range |
|--------|-------|
| Last 7 days | Past 7 days |
| Last 30 days | Past 30 days (default) |
| Last 90 days | Past 90 days |
| Last year | Past 365 days |
| All time | From first recorded data point |
| Custom | Pick any start/end dates with the date picker |

Presets beyond your plan's history window are disabled with an upgrade prompt. **All time** always works: it clamps automatically to the oldest data available on your plan and shows a notice like *"Showing last 90 days (Free plan)."*

### Granularity

Controls the width of each time bucket. Choose from **Auto**, **Day**, **Week**, **Month**, **Year**, **Cumulative**, or **Custom…** (N days).

- **Auto** selects the most readable granularity for the chosen range and displays the resolved value beneath the control (e.g., *Auto: Week*).
- **Cumulative** collapses the entire range into a single running-total line. When Cumulative granularity is active, mode is locked to Cumulative automatically.
- **Custom** reveals a numeric input (1 to 90) to define exact bucket width in days.

### Mode

| Mode | Description |
|------|-------------|
| New | Each bucket shows only the delta gained in that period. |
| Cumulative | Each bucket shows the running total from the beginning of the range. |

Rate-only metrics (engagement rate, growth rate) ignore this toggle: they always show rates.

**How the first day of a range is counted.** In New mode, a video's gain on the first day of the range is measured against its last snapshot from *before* the range, so a 30-day range shows what was gained in those 30 days. A video posted inside the range has no earlier snapshot, so everything it has counts as gained in the range, on the day it was posted.

**Days are your org's days.** Every bucket is a calendar day (or week, month, year) in your organization's analytics timezone (`analytics_tz`). A range ending today includes today, shown as a partial bucket.

### Compare vs prior period

Toggle the **vs prior period** switch to overlay a second series shifted back by the same length as the selected range. This lets you see whether this week outperformed last week, or this month versus last month. The prior-period line renders dashed in a muted color alongside the primary series.

When compare is active, a row of **Δ% summary cards** appears above the charts showing the change for each metric versus the prior period:

- **New mode** (bar charts): shows the total delta: current-period sum vs prior-period sum
- **Cumulative mode** (area charts): shows the *period gain*: (last value − first value) for each window
- **Rate/Engagement mode** (line charts): shows the pp difference: current average rate minus prior average rate, expressed as `±X.Xpp` rather than a percentage (since a percentage-of-a-percentage is misleading)

#### Partial-bucket exclusion

Today's bucket (and any in-progress bucket) is drawn dashed on the chart to indicate it's incomplete. It is **excluded from the Δ% card calculation** so partial data doesn't distort the summary. The prior-period equivalent of that bucket is also excluded, keeping the comparison fair.

#### Truncation

If the prior window extends before your tracking history or your plan's retention limit, the available prior period is automatically clamped. A muted notice appears below the toolbar indicating how many days of data were available versus requested: for example:

> *Prior period partial: 22 of 30 days available (before tracking started)*

Hovering a dashed prior-period line in this state shows a tooltip with the same context.

#### Limitations

> **Warning: Limitations**
>
> - **Compare and breakdown are mutually exclusive.** When a breakdown filter is active, the compare toggle is disabled. API calls combining `compare=prior_period` with a non-`none` breakdown return a 400 error (`compare_breakdown_exclusive`).
> - **Stacked engagement and velocity charts** do not show a prior-period overlay: these panels have different aggregation semantics not suited to simple time-shifted comparison.

---

## Retention by plan tier

Each plan tier has a maximum analytics history window:

| Plan | History window |
|------|---------------|
| Free | 90 days |
| Starter | 365 days |
| Ultra | Unlimited |

### In the UI

When you select a range that would exceed your tier's limit:

- **Preset chips** beyond your tier (e.g. "Last year" on Free) show a lock icon and are clickable to see the upgrade prompt.
- **All time** always works: it clamps to the oldest data available on your plan and shows a notice like *"Showing last 90 days (your current plan). Upgrade for longer history."* Clicking the notice opens the upgrade modal.
- **Custom date picker** allows choosing any range; if you pick a `from` date outside your window the API gate (below) will fire.

### For API and agent callers

If you request a `from` date older than your plan's retention window, the unified endpoint returns **HTTP 402**:

```json
{
  "data": null,
  "error": "Requested range exceeds plan retention window",
  "meta": {
    "code": "retention_exceeded",
    "limitKey": "stats_history_retention_days",
    "tier_retention_days": 90,
    "requested_from": "2025-01-01",
    "earliest_allowed": "2026-01-21"
  }
}
```

Use `tier_retention_days` from any successful response to pre-check the available window before issuing a long-range query.

### Downgrade grace

When an org is downgraded to a lower tier, the previous tier's retention window remains in effect for **14 days** to give users time to export data. After 14 days, the new tier's limit is enforced for both queries and the nightly data purge.

---

## Multi-entity compare

On the **Analytics** tab of a single creator or campaign you can overlay up to **5 entities** on the same charts to compare performance side-by-side. Video Analytics tabs and the Insights pages don't have this control.

### How to use it

**Step 1: Click the Compare with… button**

Click the **"Compare with…"** button in the toolbar (the icon with overlapping people).

**Step 2: Search for creators or campaigns**

Search for creators or campaigns by name.

**Step 3: Select up to 4 additional entities**

Select up to 4 additional entities: your primary entity is always pinned and shown first.

**Step 4: Each entity gets a distinct color**

Each entity gets a distinct color, stable across every chart panel on that tab.

### What changes in overlay mode

- **All count-mode charts switch to line mode**: bars and filled areas are replaced with lines so N overlapping series stay readable.
- **Engagement Rate** still renders as a line (unchanged: it's rate-mode by default).
- **Stacked engagement breakdown**, **Top Videos**, and **Top Creators** panels are hidden: these are single-entity concepts.
- **Baseline markers** show only for your primary entity.
- **The "vs prior period" compare switch is disabled** while overlay is active. The two modes are mutually exclusive.

### Shareable overlay URLs

The selected entity IDs are encoded in the URL as `?tbe=id1,id2,...`. Copying the URL and sharing it with a teammate preserves the full overlay view: same date range, granularity, and selected entities.

---

## Shareable URLs

On an Analytics tab, all toolbar state (range preset, granularity, mode, compare toggle, and custom dates) is persisted in the page URL via short query parameters. Copy the URL from your browser's address bar to share an exact chart view with a teammate: they'll land on the same date range and settings.

The toolbar also saves your last-used settings per entity type (creator, campaign, video) in your browser's local storage, so your preferred range is restored when you navigate back to that tab.

---

## `engagement_rate_by_followers` on video entities

Not supported: use `creator` or `campaign` entities instead.

## `growth_rate_pop` with follower or video count base metrics

`base_metric=followers` and `base_metric=video_count` are only supported for `creator` entities.

---

## Chart modes

The **Mode** toggle and **Breakdown** button described here are on the **Analytics** tab of a single creator, campaign or video, not on the Insights pages.

### New vs Cumulative

The **Mode** toggle changes how each time bucket is rendered:

| Mode | Chart type | What you see |
|------|-----------|--------------|
| New | Bar chart | Delta gained in that bucket only: e.g., views added this week |
| Cumulative | Area chart | Running total from the start of the selected range |

Switching modes does not change the underlying data: only how it is displayed. Use **New** to spot spikes and posting cadence; use **Cumulative** to see growth trajectory.

### Rate metrics

Metrics like **Engagement Rate** ignore the Mode toggle and always render as a line chart, since a rate is neither a delta nor a running total.

### The Partial badge

When the most recent bucket covers a time period that hasn't ended yet (e.g., a weekly bucket mid-week), the chart shows a **Partial · as of HH:mm TZ** label above the chart. The in-progress bar or area is shaded to distinguish it from completed buckets.

### Baseline markers

Vertical dotted lines on a chart mark when tracking first started for that entity (labeled **Tracking started**) or when a manual baseline snapshot was recorded (labeled **Baseline**). Data before a baseline marker represents the snapshot state, not necessarily change over time.

### Breakdowns

On a creator or campaign **Analytics** tab, the **Breakdown** button in each chart panel header lets you decompose a single aggregate series into its top constituent entities: up to 10, with everything else rolled into an **Other** segment.

| Entity scope | Available breakdown dimensions |
|---|---|
| Campaign | By video, By creator |
| Creator | By video |

**How it works:**
- Click the **Breakdown** button in any count-based chart (Total Views, Total Likes, Follower Count, Total Videos).
- The chart switches to a stacked-area view, showing each constituent as a colored band. **Other** is rendered in muted gray.
- Breakdown state is encoded in the URL (`bdv=video`, `bdl=creator`, etc.), so links are shareable.

**Constraints:**
- Breakdown is not available for rate metrics (Engagement Rate, Posts per Week, etc.). The button is disabled with a tooltip explaining why.
- Breakdown and period comparison (`compare=prior_period`) are mutually exclusive. Enabling comparison disables the breakdown menu.
- Breakdown and multi-entity overlay are mutually exclusive. The breakdown menu is disabled when two or more entities are overlaid.

### Cohort velocity comparison

On a video's **Analytics** tab, the **View Velocity** chart shows how many views a video gained per day after it was posted (day 0 = post date). Days follow your organization's time zone. Older videos are checked less often, sometimes once a week. When there are several days between two checks, the views gained are spread evenly across those days rather than shown as one spike.

Toggle **Compare to cohort** to overlay two dashed reference lines:

- **Creator avg**: average views/day for up to 10 of the creator's most recent other videos.
- **Org avg**: average views/day for videos posted in the last 90 days across your entire organization.

The averages count each check's views on the day of the check, while the video's own line spreads views evenly across the days between checks. A note under the chart says this, because the averages can look spikier. Both lines show the number of videos in the cohort in the chart legend. If a creator has only published the current video, the creator avg line is hidden and a note is shown. Org avg always appears when there is data.

### Stacked engagement breakdown

The **Engagement Breakdown** panel on Campaign and Video analytics shows **Likes, Comments, Shares, and Saves** as stacked segments. Each color maps to one metric. Use **Mode: New** to see engagement added per period, or **Mode: Cumulative** for total engagement over the range.

### Engagement Rate panel

The **Engagement Rate** panel is available on Creator and Campaign tabs. It calculates:

```text
(likes + comments + shares + saves) / views × 100
```

A value of 5% means roughly 1 in 20 views resulted in some form of engagement. This panel always renders as a line chart and is not affected by the Mode toggle.

---

## Annotations

Annotations are event markers that appear as pins at the top of each chart on the **Analytics** tab of a creator, campaign or video. The Insights pages don't show them. They help you correlate metric changes with real-world events.

### Types of annotations

| Source | Kind | Description |
|--------|------|-------------|
| **User** | `user` | Notes you add manually: title, optional date/time, optional markdown body |
| **Auto** | `creator_profile_change` | Detected changes to a creator's platform profile (bio, avatar, etc.) |
| **Auto** | `creator_milestone` | Follower/view milestones crossed by a creator |
| **Auto** | `campaign_created` | When a campaign was created |
| **Auto** | `readme_changed` | When a creator or campaign README was edited |

### Adding an annotation

1. Open the **Analytics** tab of a creator, campaign or video.
2. Click the **Annotations** button in the top-right corner of the panel.
3. Click **Add** in the popover.
4. Fill in the title (required), date & time, and optional markdown notes.
5. Click **Add annotation**: the pin appears immediately on all charts in the tab.

### Cascade behavior

Annotations follow a **cascade-down** model:

- A **campaign-scoped** annotation appears on Creator and Video tabs for members of that campaign.
- A **creator-scoped** annotation appears on Video tabs for videos by that creator.
- Cascaded annotations show a caption ("From campaign: Summer Launch") so you know their origin.

Video-scoped annotations do **not** cascade up: they stay local to that video's tab.

### Show/hide auto annotations

Each chart panel has a toggle in the **Annotations** popover labeled **Show auto annotations**. When turned off, only your user-authored notes are shown. This preference is saved in the URL (`?tbha=1`) and local storage.

### Chart pin markers

Each pin represents one or more annotations bucketed to the same time period. The badge shows how many. Click a pin to see all annotations in that bucket: title, timestamp, any markdown content, and the cascade source if applicable.

---
Source: https://www.trackagoat.com/docs/guides/analytics
