linkedin-studio/assets/analytics/README.md
Kjell Tore Guttormsen 63506f7d5c feat(linkedin-studio): N16 — out-of-network-andel + patterns-oppdatering + boundary-map [skip-docs]
Reach-splitten (in/out-of-network) er native i LinkedIns post-analytics siden juni
2026, men vises som PROSENT og finnes ikke i CSV-eksporten. Planen antok to
manuelle antall; verifiseringen viste prosent, så modellen er ett felt —
outOfNetworkPct — og in-network er komplementet.

- parseOptionalPercent: egen parser, ikke parseOptionalCount. Komma er desimal
  (36,5 -> 36.5, aldri 365), og verdi >100 avvises: i én kolonne kan ikke et
  absolutt antall skilles fra en andel, så svaret er unknown, ikke en gjetning.
  Blank/ikke-numerisk/negativ -> unknown; ekte 0 beholdes.
- Ett lagret halvpart, kryssjekket: In-network godtas og lagres som komplement;
  et transkribert par som ikke summerer til ~100 (±1 avrunding) forkastes som
  unknown i stedet for å bli halvveis trodd.
- weightedOutOfNetworkPct: impressions-vektet roll-up (avgOutOfNetworkPct, uke +
  måned). Flatt snitt lar en 50-visnings-post slå en på 10 000; poster uten
  avlesning ekskluderes, og null vekt gir undefined — aldri 0, aldri NaN.
- Reach inngår ALDRI i engagementRate (distribusjon != engasjement). Rapporten
  leser den som akvisisjon (ut) vs resonans (inn), og sier «ikke ført for denne
  perioden» framfor å estimere. En reach-innsikt går inn i N15s do-next-kanal.
- Step 7c (A2-F11): rapporten tilbyr diff mot brukerens engagement-patterns.md
  med eksplisitt go — aldri stille skriving, aldri inn i den shippede malen.
- Boundary-map (E#9): dwell eksplisitt umålbar, saves partner-gated, reach
  native men CSV-eksport uverifisert.
- Reach-frie importer er byte-identiske med før, på skjerm og på disk.

TDD: rødt bevist først (10 feilende), analytics 119 -> 144 tester, tsc ren.
test-runner 232 -> 247 (Section 16w, gulv 213 -> 228). Alle suiter grønne.
CHANGELOG: N15-oppføringen manglet og er backfilt sammen med N16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-25 15:56:04 +02:00

5.2 KiB

LinkedIn Analytics Data

This directory contains imported analytics data from LinkedIn CSV exports.

How to Import

  1. Go to LinkedIn Creator Analytics
  2. Click Export to download a CSV of your content analytics
  3. Save the CSV file to exports/ directory
  4. Run /linkedin:import in Claude Code

Optional: add per-post saves (manual)

LinkedIn's CSV export does not include saves, and there is no self-serve API to pull them — but the per-post save count is visible in your native post analytics (since ~Sept 2025). To track it, add a Saves column to the CSV and type the count you read off LinkedIn. The importer picks it up automatically when the column is present:

"Content","Date","Impressions","Reactions","Comments","Shares","Clicks","Saves"
"My post...",2026-02-10,5000,100,30,15,200,42

A missing column — or a blank Saves cell — leaves saves unknown (never counted as 0), and saves is not folded into the engagement rate (which stays comparable to older imports). Saves is the strongest organic engagement signal, so the reports surface it as its own line. Dwell time stays unmeasurable — it is internal to LinkedIn for organic posts, with no count to transcribe.

Optional: add the out-of-network reach share (manual)

LinkedIn splits a post's impressions into in-network (people who already follow or are connected to you) and out-of-network (people who found it through recommendations, reshares or search). It sits in your post analytics under Discovery, beneath the impressions count — a global rollout that began in June 2026 — and it is shown as percentages, not as two counts. It is not in the CSV export, and whether it will ever be exported is unverified.

To track it, add an Out-of-network column and type the percentage you read off that panel. The % sign is optional:

"Content","Date","Impressions","Reactions","Comments","Shares","Clicks","Out-of-network"
"My post...",2026-03-10,5000,100,30,15,200,37%

Details worth knowing:

  • Either half works. An In-network column is accepted instead and stored as its complement (In-network 63% → out-of-network 37). Only the out-of-network share is kept, because the two halves describe one split — storing both would let a record contradict itself.
  • Both halves are cross-checked. If you transcribe both and they do not sum to ~100 (one point of rounding slack allowed), the reading is discarded as unknown rather than guessing which cell was misread.
  • Percent, not a count. A value above 100 is refused (unknown): in one column an absolute impression count and a share are indistinguishable, so the importer will not guess. A comma is read as a decimal mark (36,5 → 36.5%).
  • Unknown is never 0. A missing column, a blank cell, or a non-numeric cell leaves the share unknown. A genuine 0 is kept — nothing left your network.
  • Not part of the engagement rate. Reach is a distribution signal, not engagement: a high out-of-network share means the post acquired new audience, while high in-network engagement means it deepened the audience you already have. Roll-ups are impressions-weighted, so a small post with a high share cannot outvote a large one.

Directory Structure

analytics/
├── exports/           # Place LinkedIn CSV exports here
├── posts/             # Auto-generated: imported post data (JSON)
├── weekly-reports/    # Auto-generated: weekly performance reports (JSON)
└── README.md          # This file

Data Format

Post Analytics (posts/*.json)

Each file contains a batch of imported posts:

{
  "batchId": "batch-...",
  "importedAt": "2026-01-29T...",
  "exportFilename": "content-analytics.csv",
  "dateRange": { "from": "2026-01-13", "to": "2026-01-28" },
  "postCount": 8,
  "posts": [
    {
      "id": "abc123",
      "title": "First 100 chars of post...",
      "publishedDate": "2026-01-28",
      "metrics": {
        "impressions": 4523,
        "reactions": 87,
        "comments": 23,
        "shares": 12,
        "clicks": 156,
        "engagementRate": 6.15
      }
    }
  ]
}

metrics.saves is optional — present only on posts where you supplied a Saves column value (see "Optional: add per-post saves" above). Posts without it omit the field entirely, so older imports round-trip unchanged.

Weekly Reports (weekly-reports/*.json)

Generated via /linkedin:report. Contains:

  • Summary metrics (totals, averages)
  • Top and underperforming posts
  • Week-over-week trends
  • Performance alerts (spikes, drops)

CLI Usage

The analytics CLI can also be invoked directly:

# Import a CSV export
ANALYTICS_ROOT=./assets/analytics node --import tsx scripts/analytics/src/cli.ts import <filename>

# Generate weekly report
ANALYTICS_ROOT=./assets/analytics node --import tsx scripts/analytics/src/cli.ts report --week 2026-W05

# Analyze trends
ANALYTICS_ROOT=./assets/analytics node --import tsx scripts/analytics/src/cli.ts trends --period month --metric impressions

Privacy

All data in this directory (except this README) is gitignored. Your analytics data stays local.