Skip to content

fix(analytics): use server-aggregated creator analytics endpoint#375

Open
rabble wants to merge 20 commits into
mainfrom
fix/creator-analytics-aggregate-endpoint
Open

fix(analytics): use server-aggregated creator analytics endpoint#375
rabble wants to merge 20 commits into
mainfrom
fix/creator-analytics-aggregate-endpoint

Conversation

@rabble
Copy link
Copy Markdown
Member

@rabble rabble commented May 18, 2026

Summary

The creator analytics dashboard was failing with "Funnelcake bulk stats error: 400" for active creators. Root cause: useCreatorAnalytics paginated up to 4×50=200 of the creator's videos and POSTed all those IDs to /api/videos/stats/bulk, which (a) silently truncated totals for anyone with more than 200 uploads and (b) returned HTTP 400 "Maximum 100 event_ids allowed" once a creator crossed 100 uploads.

Switch to the existing NIP-98 endpoint GET /api/users/{pubkey}/analytics (handler lives in funnelcake at crates/api/src/handlers.rs:6227, registered at router.rs:734). It returns server-aggregated totals, timeseries, and a top-N posts list over the full catalogue. Hydrate the top-N IDs (≤10) via POST /api/videos/bulk for title/thumbnail rendering.

What changes

  • Network: 5 requests (4 pages + bulk_stats) → 3 (analytics + profile in parallel, then bulk_videos for top-N)
  • Payload: ~200 video blobs + ~200 stat blobs → 1 summary + ≤10 metadata blobs
  • Correctness: totals now span the whole catalogue; no 100/200-video cliff
  • Auth: hook now also waits on signer (the analytics endpoint is NIP-98); dashboard page already gates on user?.pubkey so this is a sub-second wait

Files

  • src/config/api.ts — +userAnalytics, +bulkVideos endpoint paths
  • src/types/funnelcake.ts — types mirroring CreatorAnalyticsResponse / CreatorAnalyticsSummary / CreatorAnalyticsTimeseries / CreatorTopPost
  • src/types/creatorAnalytics.ts — +CreatorAnalyticsWindow type, +window field on CreatorAnalyticsData
  • src/lib/funnelcakeClient.ts — renamed private helper authenticatedNotificationRequestauthenticatedFunnelcakeRequest (it's no longer notifications-only; 3 internal call sites updated); added fetchCreatorAnalytics (NIP-98) and fetchBulkVideos
  • src/lib/analyticsTransform.ts — replaced merge/compute path with kpisFromAnalytics + topPostToPerformance + new buildAnalyticsData(analytics, topMeta, profile) signature
  • src/hooks/useCreatorAnalytics.ts — dropped the paginator; pulls signer from useCurrentUser; queryKey now carries window; enabled requires both pubkey and signer
  • src/lib/analyticsTransform.test.ts — rewritten for the new transform surface (8 tests)

AnalyticsPage.tsx did not need changes — the hook's public signature is backward-compatible (window param defaults to 30d).

Test plan

  • npx tsc --noEmit — clean
  • npx vitest run src/lib/analyticsTransform.test.ts src/lib/funnelcakeClient.test.ts — 30/30 pass
  • npx eslint on all 7 changed files — 0 issues
  • Manual smoke: log in as a creator with >100 videos, hit /analytics, confirm totals render and reflect the full catalogue
  • Manual smoke: log in as a creator with <10 videos, hit /analytics, confirm totals still render and match the prior dashboard
  • Confirm top-N rows still link to the correct videos (thumbnail/title come from bulk_videos)

🤖 Generated with Claude Code

rabble and others added 20 commits May 8, 2026 15:50
Adds Filipino as a supported locale (code `fil`, label "Filipino")
with brand-voice translations across all 11 namespaces.

- `tl`, `tl-PH`, and `fil-PH` browser locales all alias to `fil`
  so users on older or newer OS settings get matched.
- Voice tuned for PH internet-native users: Taglish where natural
  ("Loops ng mga sinusundan mo.", "Tara, sumama sa community"),
  English kept for nav/category labels (matches TikTok/IG/FB-PH
  convention), formal register only on legal/policy section
  titles where it belongs.
- Existing locale-parity test covers key completeness; alias
  resolution test added in config.test.ts.

A native PH reviewer should sanity-check `common.json` brand
strings before this ships ("Uwi na tayo.", "Sagot ka namin.",
the NIP-44 signer fallback paragraph).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nto i18n

Two issues surfaced when QAing the Filipino locale on a deploy preview:
many UI strings still rendered in English even with Filipino selected.

Cause #1: my initial Taglish strategy left nav/categories/discovery labels
in English (Music/Sports/Discover/Categories/Hot/Tags etc.). Other locales
follow a full-translation pattern (Spanish: Inicio/Descubrir/Categorias/
Musica), so Filipino now matches: Tahanan/Tuklasin/Mga Kategorya/Musika/
Sayaw/Komedya/Paglalakbay/Pamilya/Pagkain/Teknolohiya/etc. Sports, Fashion,
and Profile/Analytics stay as loanwords where Tagalog has no commonly-used
native term.

Cause #2: four prominent strings were hardcoded in components and never
went through i18n at all. Lifted them into the catalog and wired t():
- "Log in" (LoginArea) → auth.logIn
- "Classic Viners" (ClassicVinersRow header) → discovery.classicViners
- "Play all" (VideoFeed + SearchPage compilation buttons) → common.playAll
- "Archived" + tooltip (VineBadge) → vineBadge.archived / vineBadge.tooltip

New keys added to all 16 locales (English placeholders for the 14 non-fil
non-en locales — they can be properly translated in follow-up PRs).

Also addressed PR review feedback on the alias logic:
- Removed redundant `fil: 'fil'` entry from LOCALE_ALIASES (never reached
  because SUPPORTED_LOCALES includes 'fil' and is checked first).
- Split the alias test into two: one for legacy `tl`/`tl-PH` aliases
  (LOCALE_ALIASES path), one for `fil-PH` regional variants
  (standard split-on-dash fallback). Was misleading to lump them together.

Test setup: VideoFeed/SearchPage/LoginArea tests now init i18n and stub
localStorage in beforeEach (matching the AppSidebar.test.tsx pattern).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts every user-facing string in VideoCard into the i18n catalog
under the new `videoCard` namespace in common.json:

- 12 aria-labels (subtitles, mute, fullscreen, like/unlike, repost,
  view-likes/reposts, comment, share, download, lists, more options)
- 7 toast title/description pairs (mute success/fail, pin/unpin
  success/fail, download error/start)
- 8 dropdown menu items (pin/unpin, delete, report video/user, send
  via DM, view source, mute user)
- Repost-attribution strings with i18n plural support
  (repostedBySingle / repostedByMultiple_one / _other)
- Failed-load + retry copy, age-gate action labels, loading-profile
  fallback, view-source dialog title

Filipino translations follow the brand-aligned full-translation
pattern (e.g., "I-mute si {name}", "Burahin ang video", "Wala na
sa feed mo si {name}.").

The 14 non-en/non-fil locales got English placeholders for now —
quality translations should follow in per-locale review PRs.

Test setup: VideoCard.test.tsx now inits i18n + stubs localStorage
in beforeEach (matching the LoginArea/VideoFeed/SearchPage pattern
established earlier in this branch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…llel agents

Second pass of i18n rollout: dispatched 5 parallel subagents to wire
high-impact dialog and profile surfaces. Each agent edited its
component(s) directly and emitted a JSON patch to /tmp/i18n-patches/;
patches were aggregated and merged into all 16 locales centrally to
avoid race conditions on common.json.

Components wired (with new namespace per component):
- addToListDialog (27 keys)
- createListDialog (31 keys)
- editListDialog (29 keys)
- zapDialog (20 keys)
- walletModal (31 keys)
- profileHeader (26 keys)
- loginDialog (41 keys)
- accountSwitcher (7 keys)
- reportContentDialog (26 keys)
- deleteVideoDialog (10 keys)
- viewSourceDialog (13 keys)
- ageVerificationOverlay (8 keys)
- badgeDetailModal (4 keys)

Total: 273 keys × 16 locales.

Filipino translations follow the established pattern (Spanish-style
full translation, Tagalog `I-` prefix on English verbs where natural,
brand-voice casual-direct tone). The 14 non-en/non-fil locales got
English placeholder values for the new keys — translation-quality
follow-up needed per locale.

Test setup: 5 component test files that touched the new t() calls
got the standard localStorage stub + initializeI18n boilerplate added
to beforeEach.

Verification:
- 727/727 vitest tests pass
- tsc --noEmit clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… agents

Batch 2 of i18n rollout. Five parallel subagents wired:
- Settings: ModerationSettingsPage, LinkedAccountsSettingsPage, LinkedAccounts (100 keys)
- Analytics: AnalyticsPage, LeaderboardPage, DebugVideoPage (57 keys)
- Forms: EditProfileForm, VideoMetadataForm, UploadPage (55 keys)
- Content pages: VideoPage, ProfilePage, CategoryPage, HashtagPage,
  ListDetailPage, ConversationPage (160 keys, with plural forms)
- Feed components: VideoCommentsModal, VideoPlayer, ThumbnailPlayer,
  HashtagExplorer, ContentOriginBadges, ProofModeBadge,
  PinnedVideosSection (46 keys)

New namespaces (22 total): moderationSettings, linkedAccountsSettings,
linkedAccounts, analyticsPage, leaderboardPage, debugVideoPage,
editProfileForm, videoMetadataForm, uploadPage, videoPage, profilePage,
categoryPage, hashtagPage, listDetailPage, conversationPage,
videoCommentsModal, videoPlayer, thumbnailPlayer, hashtagExplorer,
contentOriginBadges, proofModeBadge, pinnedVideosSection.

Filipino translations follow the established Spanish-style full-
translation pattern (Tagalog `I-` prefix on English verbs, brand-voice
casual-direct). The 14 non-en/non-fil locales got English placeholders
for these new keys — translation pass coming next in this PR.

Test setup: ThumbnailPlayer + VideoPlayer test files (which still
contained hardcoded text assertions) got the standard localStorage
stub + initializeI18n boilerplate added to beforeEach.

Verification:
- 727/727 vitest tests pass
- tsc --noEmit clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous two batches lifted ~1,200 strings into the i18n catalog
and translated them to en + fil. The 14 other locales (ar, de, es, fr,
id, it, ja, ko, nl, pl, pt, ro, sv, tr) had been carrying English
placeholder values for the new keys, which produced a degraded mixed-
language UX (existing nav/menu translated; new dialogs/profile/settings
in English).

This commit dispatches 14 parallel translation agents — one per locale
— each translating the full set of new keys from the en source. Every
agent reported 766/766 leaves translated, all interpolation placeholders
({{name}}, {{count}}, etc.) preserved verbatim, all i18next plural
suffix keys (_one/_other) intact, and brand/protocol terms (Divine,
Vine, Nostr, NIP-*, ProofMode, sats, npub, etc.) left untouched.

14 × 766 = 10,724 translation entries applied across these namespaces:
common.playAll, auth.logIn, discovery.classicViners, vineBadge,
videoCard, addToListDialog, createListDialog, editListDialog,
zapDialog, walletModal, profileHeader, loginDialog, accountSwitcher,
reportContentDialog, deleteVideoDialog, viewSourceDialog,
ageVerificationOverlay, badgeDetailModal, moderationSettings,
linkedAccountsSettings, linkedAccounts, analyticsPage, leaderboardPage,
debugVideoPage, editProfileForm, videoMetadataForm, uploadPage,
videoPage, profilePage, categoryPage, hashtagPage, listDetailPage,
conversationPage, videoCommentsModal, videoPlayer, thumbnailPlayer,
hashtagExplorer, contentOriginBadges, proofModeBadge,
pinnedVideosSection.

Tone targets per locale (from agent prompts): casual-direct in all
languages, informal address (tu/du/sen/kamu/ty/etc., not formal),
brand voice "never corporate". Existing pre-PR strings are untouched.

⚠️ NATIVE-SPEAKER REVIEW STILL RECOMMENDED before broad campaigns in
each market. AI translations are high-quality on average but may
include awkward phrasing, register mismatches, or incorrect idioms
that only a native reviewer will catch. File follow-up issues per
locale for native review.

Verification:
- 727/727 vitest tests pass
- tsc --noEmit clean
- Locale-parity test passes (every locale has every English key)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Batch 3 of the i18n rollout (5 parallel agents) wired:
- Pages K (4): Index, NotificationsPage, GetEmbedPage, TrendingPage (~56 strings)
- Pages L (6): MerchPage, EventPage, UniversalUserPage, AtUsernamePage, AppCallbackPage, AuthCallbackPage (~58 strings)
- Landing/badges M (5): AuthenticDemo, VerifiedDemo, DecentralizedDemo, OriginalContentBadge, VideoVerificationBadgeRow (~15 strings)
- Input/recording N (5): CameraRecorder, comments/Comment, PWAInstallPrompt, RelaySelector, SocialLinks (~50 strings)
- Misc O (6): FullscreenVideoItem, PrivacyPage chrome, TermsPage chrome, _BrandPreview (no-op, dev-only), ui/sidebar, ui/pagination (~24 strings)

26 new namespaces, ~216 keys × 16 locales (en + fil + 14 AI translations).

Translation pass: 14 parallel agents (one per locale: ar, de, es, fr,
id, it, ja, ko, nl, pl, pt, ro, sv, tr) translated the new 216 keys.
All reported 216/216 leaves translated, all interpolation placeholders
preserved, all `_one`/`_other` plural suffixes intact, all brand names
(Divine, Vine, Nostr, NIP-*, ProofMode, App Store, Google Play,
Bonfire, npub, sats, etc.) untouched.

Test setup:
- PWAInstallPrompt.test.tsx now inits i18n in beforeEach
- static-pages-i18n.test.tsx updated: the previous "keeps privacy copy
  in english" / "keeps terms copy in english" tests asserted the
  pre-i18n behavior of those pages. Page titles and "last updated"
  labels are now translated (chrome-only); legal body paragraphs
  remain hardcoded English. Tests now verify the translated chrome
  with a comment explaining the chrome-only scope.

Verification:
- 727/727 vitest tests pass
- tsc --noEmit clean
- Locale-parity test passes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…components

CI build was failing because the main JS bundle hit 3.87 MB after this
PR's i18n catalog growth (16 locales × ~1500 keys, eager-loaded via
import.meta.glob). The VitePWA workbox precache limit was 3 MB, so the
service-worker generation step refused to precache the asset, failing
the build.

- Bumped maximumFileSizeToCacheInBytes to 5 MB (with a comment flagging
  the longer-term fix: lazy-load locale JSON instead of eager glob).

Also lands batch 4 of the i18n component-wiring rollout — 9 small
remaining components/UI primitives:
- FullscreenFeed (6 toast strings)
- DeleteCommentDialog (10 strings)
- InviteCodeForm (5 strings)
- ApplePodcastEmbed (7 strings)
- VideoCardWithMetrics (2 toast strings)
- ui/sheet, ui/dialog, ui/breadcrumb, ui/carousel (sr-only labels)

35 keys × 16 locales added. The 14 non-en/non-fil locales got English
placeholders for these new keys; a follow-up translation pass will fill
those in (consistent with how earlier batches in this PR were handled).

Test setup: FullscreenFeed.test.tsx now inits i18n in beforeEach.

Verification:
- 727/727 vitest pass
- npm run build succeeds locally (3.9 MB main bundle, under new 5 MB limit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final translation pass for this branch. 14 parallel translation agents
translated the 36 batch-4 keys (FullscreenFeed toasts, DeleteCommentDialog,
InviteCodeForm, ApplePodcastEmbed, VideoCardWithMetrics, plus shadcn
ui/sheet/dialog/breadcrumb/carousel sr-only labels) into ar, de, es, fr,
id, it, ja, ko, nl, pl, pt, ro, sv, tr.

All agents reported 36/36 leaves translated, all interpolation
placeholders ({{showName}}) and brand names (Divine, Vine, Apple
Podcasts, podcast episode titles) preserved.

⚠️ Native-speaker review still recommended for all 14 AI-translated
locales before broad market campaigns.

Verification:
- 727/727 vitest tests pass
- tsc --noEmit clean
- npm run build succeeds (3.9 MB main bundle, under 5 MB workbox limit)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds api.divine.video's new period parameter (now/today/week/month/all) as a
new "Popular" tab on /trending, deep-linkable via ?sort=&period=. Drops
Controversial from visible tabs (off-brand engagement-bait).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12-task TDD plan covering type changes, client/hook plumbing,
URL-driven page state, controversial coercion, empty state,
i18n, a11y, and verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Task 2: switch funnelcakeClient tests to existing dynamic-import +
  global.fetch pattern (vi.spyOn would conflict with module mocks)
- Task 3: add mockFetchVideosV2 — current test mock omits it, so any
  trending-feed test silently calls undefined()
- Task 6: drop unused period icon imports (pills are text-only)
- Task 8: encode New tab as ?sort=new (fixes round-trip bug where
  clicking New silently reverted to Hot); drop the brittle URL-rewrite
  useEffect; add LocationProbe + tests for URL behavior
- Task 9: extend the existing empty-state branch instead of duplicating
  the wrapper; concrete insertion point and test guidance
- New "Risks & gotchas" section covering DiscoveryPage spillover,
  edge-feed cache, mock-coverage gap, and brand guardrails

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Task 2: fix step numbering (Step 3 was missing — went from 2→4)
- Task 8: switch period-pill tests to data-testid selectors so they
  don't depend on the i18n mock returning real translations
  (the mock returns t(key)=>key, which broke /this week/i regexes).
  Add data-testid attributes to the page implementation accordingly.
  Strengthen ?sort=controversial test to also assert period row hidden.
- Task 9: replace the i18n-key-alternation selector with the actual
  EN translation ("Quiet hour"), since VideoFeed.test.tsx initializes
  real i18n via initializeI18n. Add a negative test (popular+week
  should NOT show quiet-hour empty state).
- Risks: explicit list of files that still reference 'controversial'
  in code (useInfiniteSearchVideos, useInfiniteVideos, useVideoProvider,
  useVideoByIdFunnelcake, relayCapabilities) — none need editing
  because the SortMode type literal is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the unnecessary "add mockFetchVideosV2.mockReset()"
  instruction — the existing vi.clearAllMocks() in beforeEach
  (line 56 of useInfiniteVideosFunnelcake.test.ts) already
  clears all vi.fn() call history.
- Fix the manual-QA Step 6 wording: it claimed the URL would
  rewrite from ?sort=controversial to ?sort=hot, but the actual
  design (Task 8) is that coercion is render-only. Updated to
  match the implementation.
- Same fix in the PR body's test plan checklist.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Conventions: add the project's mandated Co-Authored-By footer
  (per CLAUDE.md: "Claude Opus 4.5 <noreply@anthropic.com>"). The
  prior conventions block only described the subject-line format
  and would have produced commits missing the AI-assist trailer.
- File-structure table: drop the dangling useVideoProvider.test.ts
  row (the file exists, no task touches it, and the new 'popular'
  branch in mapToFunnelcakeSortMode is exercised end-to-end via
  Task 8's URL-state tests). Added VideoFeed.test.tsx to the table
  to reflect the empty-state work in Task 9.
- Task 7: drop dead trendingPage.popular.tabLabel and
  .tabDescription i18n keys — the page renders mode.label /
  mode.description from the EXTENDED_SORT_MODES constant
  (hardcoded EN), matching the existing pattern. Including these
  keys would have wasted a translation round in Task 10 across
  14 locales for strings nothing reads.
- Task 10 German example: replace the dropped tab-* keys with
  period.* translations, which are the strings actually used.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- File-structure table: change funnelcakeClient.test.ts row from
  "(new or extended)" to "(extend — file already exists)" to
  match what Task 2 actually does. The existing wording was a
  leftover from the first draft and could have led the executor
  to briefly consider creating a duplicate test file.

The plan has now converged — no other substantive issues found
on this pass. Verified:
- jest-dom matchers (.toBeInTheDocument, .toHaveAttribute) are
  set up in src/test/setup.ts, so the new tests' DOM assertions
  will work
- vi.clearAllMocks() in the existing useInfiniteVideosFunnelcake
  test beforeEach clears call history but not mockReturnValue;
  the existing pattern re-sets defaults each beforeEach, and our
  new tests follow that pattern
- queryKey changes (period added) trigger fresh TanStack Query
  state per period — desired UX for switching windows
- Recommendations test path doesn't exercise fetchVideosV2, so
  adding the mock is safe and won't change existing assertions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Real bug found that prior passes missed:

useInfiniteVideosFunnelcake.ts:210-234 unconditionally consumes
window.__DIVINE_FEED__ on the first trending page, regardless of
sortMode or period. The Fastly edge worker injects this for the
default trending feed (sort=watching, no period). Result: a user
who deep-links to /trending?sort=popular&period=today would see
the wrong sort on page 1 (edge default) and only get Popular Today
starting on page 2. Confusing, broken UX for the most common entry
point (shared/bookmarked URLs).

My earlier "Risks & gotchas" note had claimed the queryKey change
made this safe. That was wrong — the edge consumption is inside
queryFn before any queryKey-driven cache lookup.

Fix: Task 3 gains a new Step 7 that guards edge consumption with
`!period`, plus a regression test asserting fetchVideosV2 is
called and the edge cache is left intact when period is set.
Existing Hot/Top/Rising sorts keep their edge behavior — they
already accept the same kind of mismatch in production and this
plan deliberately doesn't expand the scope.

Renumbered Steps 7-9 → 8-10. Updated Risks section to reflect
that the bug is fixed by the plan, not just documented.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The Task 3 Step 7 fix added in the previous pass needs a manual
QA step in Task 12 that actually exercises the bug. The edge
worker only injects __DIVINE_FEED__ on initial HTML responses,
not on client-side SPA navigations — so testing the fix requires
opening a fresh tab to the deep link, not clicking around.

The new Step 8 in Task 12's manual verification:
1. Open a fresh tab to /trending?sort=popular&period=week
2. Verify the first API call includes sort=popular&period=week
3. Verify window.__DIVINE_FEED__ is still defined (untouched
   because the hook skipped it)

Without this, a regression in the edge guard would slip past
unit tests (which mock window) and only surface in production
when the first user reports "the popular page shows the wrong
sort on first load."

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…vior

Verified against compute-js/src/index.js:204-207: the Fastly edge
worker injects BOTH window.__DIVINE_FEED__ and __DIVINE_FEED_TYPE__
together, with the latter set to a string ("trending" by default,
or one of the discovery-feed values). The previous version of the
regression test set __DIVINE_FEED__ but explicitly deleted
__DIVINE_FEED_TYPE__ — that exercises the hook's fallback branch
(edgeFeedType === undefined && feedType === 'trending') instead of
the realistic branch (edgeFeedType === feedType).

Updated the test fixture to:
1. Set __DIVINE_FEED_TYPE__='trending' to match the production
   injection shape, exercising the realistic code path
2. Tighten the cleanup to delete both globals (matches what the
   hook would have done if it had consumed the cache)
3. Use a typed EdgeWindow alias so the casts are less noisy
4. Add a comment pointing to the edge worker source line so a
   future reader can verify the assumption

A regression that broke the realistic branch of the guard would
have slipped past the previous test version. Now caught.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The dashboard was paginating up to 4×50=200 videos and POSTing every ID
to /api/videos/stats/bulk, which (a) silently truncated totals for any
creator with more than 200 uploads and (b) failed outright with HTTP 400
"Maximum 100 event_ids allowed" once a creator crossed 100 uploads -
the symptom rendered as "Funnelcake bulk stats error: 400" on the page.

Switch to the existing NIP-98 endpoint GET /api/users/{pubkey}/analytics,
which returns server-aggregated totals, timeseries, and top-N IDs over
the whole catalogue. Hydrate just the top-N IDs via /api/videos/bulk for
title/thumbnail rendering. Net: 3 requests instead of 5, no per-creator
size cliff, and the totals are correct regardless of catalogue size.

- config: add userAnalytics + bulkVideos endpoint paths
- types: mirror CreatorAnalyticsResponse / TopPost / summary / timeseries
- funnelcakeClient: rename private auth helper to be generic across
  Funnelcake endpoints, add fetchCreatorAnalytics + fetchBulkVideos
- analyticsTransform: replace the merge/compute path with
  kpisFromAnalytics + topPostToPerformance reading the aggregate
- useCreatorAnalytics: drop the paginator; require signer from
  useCurrentUser; queryKey now carries the window
- tests: rewrite for the new transform surface (30 passing)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying divine-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: e4cb631
Status: ✅  Deploy successful!
Preview URL: https://e9239eed.divine-web.pages.dev
Branch Preview URL: https://fix-creator-analytics-aggreg.divine-web.pages.dev

View logs

@github-actions
Copy link
Copy Markdown

🚀 Preview Deployment

Property Value
Preview URL https://5c75b91f.divine-web-fm8.pages.dev
Commit e4cb631
Branch fix/creator-analytics-aggregate-endpoint

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant