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
This commit is contained in:
parent
b45fdad911
commit
63506f7d5c
21 changed files with 841 additions and 13 deletions
|
|
@ -319,6 +319,57 @@ describe("weekly", () => {
|
|||
assert.equal(report.summary.totalSaves, undefined, "Saves-free data must not introduce a totalSaves field");
|
||||
});
|
||||
|
||||
test("should roll out-of-network shares up as an impressions-weighted average", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const posts: PostAnalytics[] = [
|
||||
createTestPost({
|
||||
id: "reach1",
|
||||
publishedDate: "2026-01-12", // 2026-W03
|
||||
metrics: { impressions: 10000, reactions: 500, comments: 100, shares: 50, clicks: 200, engagementRate: 8.5, outOfNetworkPct: 20 },
|
||||
}),
|
||||
createTestPost({
|
||||
id: "reach2",
|
||||
publishedDate: "2026-01-13", // 2026-W03
|
||||
metrics: { impressions: 1000, reactions: 50, comments: 10, shares: 5, clicks: 20, engagementRate: 8.5, outOfNetworkPct: 80 },
|
||||
}),
|
||||
createTestPost({
|
||||
id: "reach3",
|
||||
publishedDate: "2026-01-14", // 2026-W03 — no share entered; must not dilute.
|
||||
metrics: { impressions: 5000, reactions: 250, comments: 50, shares: 25, clicks: 100, engagementRate: 8.5 },
|
||||
}),
|
||||
];
|
||||
|
||||
saveBatch(tempDir, createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-14" }, posts }));
|
||||
|
||||
const report = generateWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.equal(
|
||||
report.summary.avgOutOfNetworkPct,
|
||||
25.5,
|
||||
"Should weight by impressions (25.5), not average the shares flat (50)"
|
||||
);
|
||||
});
|
||||
|
||||
test("should leave avgOutOfNetworkPct undefined when no post carries a share (backward-compat)", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
const posts: PostAnalytics[] = [
|
||||
createTestPost({ id: "noreach1", publishedDate: "2026-01-12" }),
|
||||
createTestPost({ id: "noreach2", publishedDate: "2026-01-13" }),
|
||||
];
|
||||
|
||||
saveBatch(tempDir, createTestBatch({ dateRange: { from: "2026-01-12", to: "2026-01-13" }, posts }));
|
||||
|
||||
const report = generateWeeklyReport(tempDir, "2026-W03");
|
||||
|
||||
assert.equal(
|
||||
report.summary.avgOutOfNetworkPct,
|
||||
undefined,
|
||||
"Reach-free data must not introduce an avgOutOfNetworkPct field"
|
||||
);
|
||||
});
|
||||
|
||||
test("should identify top performers and underperformers", () => {
|
||||
tempDir = setupTempDir();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue