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

138 lines
5.2 KiB
Markdown

# LinkedIn Analytics Data
This directory contains imported analytics data from LinkedIn CSV exports.
## How to Import
1. Go to [LinkedIn Creator Analytics](https://www.linkedin.com/analytics/creator/content/)
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:
```json
{
"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:
```bash
# 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.