@@ -28,13 +28,17 @@ import {
2828 buildDeterministicFallback ,
2929 composeReply ,
3030} from "@/api/v2/agent-templates/services/compose-reply" ;
31- import { capturePostHog } from "@/api/v2/agent-templates/services/posthog" ;
31+ import {
32+ capturePostHog ,
33+ type PostHogCaptureProperties ,
34+ } from "@/api/v2/agent-templates/services/posthog" ;
3235import {
3336 callGenerateTemplate ,
3437 getModel ,
3538 type GenerateTemplateInput ,
3639} from "@/api/v2/agent-templates/services/templateGen" ;
3740import { GENERATION_EXECUTOR_TIMEOUT_MS , GENERATION_TTL_HOURS } from "@/config" ;
41+ import { ADMIN_ACCOUNT_ID } from "@/utils/constants" ;
3842import logger from "@/utils/logger" ;
3943import { prisma } from "@/utils/prisma" ;
4044import { validateSlug } from "@/utils/reserved-slugs" ;
@@ -109,6 +113,65 @@ interface TwitterContext {
109113 idea ?: string ;
110114}
111115
116+ // ---------------------------------------------------------------------------
117+ // PostHog actor-attribution fields — shared across every capture site
118+ // ---------------------------------------------------------------------------
119+
120+ /** Generation row shape consumed by `postHogBase` — the actor-attribution
121+ * inputs only. Declared as a structural subset of the Prisma row so this
122+ * helper is decoupled from the full schema. */
123+ interface PostHogActorSource {
124+ ownerAccountId : string ;
125+ source : string ;
126+ twitterContext : unknown ;
127+ clientDeviceId : string | null ;
128+ }
129+
130+ /**
131+ * Build the actor-attribution + always-on fields shared across every
132+ * PostHog capture site in this pipeline. Centralised so the four sites
133+ * (generate-fail, persist-fail, timeout-race-fail, success) stay in sync —
134+ * adding a new actor signal only needs one edit here.
135+ *
136+ * `isAnonymous` is derived from the admin-account sentinel: anonymous
137+ * submissions are owned by `ADMIN_ACCOUNT_ID` so the row has a valid
138+ * owner FK, but the value is a system identity, not a real user.
139+ * `resolveActor` in posthog.ts skips ownerAccountId when this is set.
140+ */
141+ function postHogBase ( args : {
142+ generation : PostHogActorSource ;
143+ requestId : string ;
144+ inputType : "text" | "pdfBase64" | "imageBase64" ;
145+ } ) : Pick <
146+ PostHogCaptureProperties ,
147+ | "requestId"
148+ | "inputType"
149+ | "source"
150+ | "ownerAccountId"
151+ | "isAnonymous"
152+ | "clientDeviceId"
153+ | "twitterUserId"
154+ > {
155+ const { generation, requestId, inputType } = args ;
156+ const twitterCtx = generation . twitterContext as TwitterContext | null ;
157+ return {
158+ requestId,
159+ inputType,
160+ source : generation . source ,
161+ ownerAccountId : generation . ownerAccountId ,
162+ isAnonymous : generation . ownerAccountId === ADMIN_ACCOUNT_ID ,
163+ clientDeviceId : generation . clientDeviceId ?? undefined ,
164+ // Lowercase + strip optional leading `@` so handle casing/format drift
165+ // on the Twitter side doesn't fragment a single user across multiple
166+ // PostHog persons. We don't currently have a stable numeric user ID
167+ // from the twitter bot — once it's threaded through, replace this with
168+ // `twitter:<numericUserId>` for true rename-resilient attribution.
169+ twitterUserId : twitterCtx ?. twitterHandle
170+ ? twitterCtx . twitterHandle . toLowerCase ( ) . replace ( / ^ @ / , "" )
171+ : undefined ,
172+ } ;
173+ }
174+
112175// ---------------------------------------------------------------------------
113176// Input coalescing — same priority as today's generate-template handler
114177// ---------------------------------------------------------------------------
@@ -395,10 +458,7 @@ async function _runPipeline(
395458 promptTokens : 0 ,
396459 completionTokens : 0 ,
397460 latencyMs : Math . round ( performance . now ( ) - startTime ) ,
398- requestId : generationId ,
399- source : generation . source ,
400- ownerAccountId : generation . ownerAccountId ,
401- inputType,
461+ ...postHogBase ( { generation, requestId : generationId , inputType } ) ,
402462 outcome : "failed" ,
403463 } ) ;
404464 throw new Error (
@@ -428,10 +488,7 @@ async function _runPipeline(
428488 } catch ( err ) {
429489 capturePostHog ( {
430490 ...templateResult . metrics ,
431- requestId : generationId ,
432- source : generation . source ,
433- ownerAccountId : generation . ownerAccountId ,
434- inputType,
491+ ...postHogBase ( { generation, requestId : generationId , inputType } ) ,
435492 outcome : "failed" ,
436493 } ) ;
437494 throw new Error (
@@ -501,21 +558,15 @@ async function _runPipeline(
501558 }
502559 capturePostHog ( {
503560 ...templateResult . metrics ,
504- requestId : generationId ,
505- source : generation . source ,
506- ownerAccountId : generation . ownerAccountId ,
507- inputType,
561+ ...postHogBase ( { generation, requestId : generationId , inputType } ) ,
508562 outcome : "failed" ,
509563 } ) ;
510564 return ;
511565 }
512566
513567 capturePostHog ( {
514568 ...templateResult . metrics ,
515- requestId : generationId ,
516- source : generation . source ,
517- ownerAccountId : generation . ownerAccountId ,
518- inputType,
569+ ...postHogBase ( { generation, requestId : generationId , inputType } ) ,
519570 outcome : "done" ,
520571 } ) ;
521572 logger . info (
0 commit comments