mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
4c5873c7cc
* fix(chat): 失败的那一轮不再自称「已完成」
一轮跑挂了,壳头写着「运行失败」。用户接着再发一条消息之后,这一轮的页脚上
多出一枚「✓ 已完成」,还戴着成功那一档的绿勾 —— 同一屏上,壳头说失败、页脚
说成功。
为什么发了下一条才出现:页脚让位的第一条例外是「这一轮拥有那张报错卡」,而
报错卡的归属(`errorCardOwnerId`)要求这条失败助手消息正好是转录的最后一条,
追加任何一条消息就变 null。而页脚那条文案阶梯里只认识 `canceled` 一个终态,
`failed` 一个字都没有,于是直落最后那个 `doneLabel`。
裁决是形态 A —— 失败轮不出这一行的状态词:
· `chat-panel-next.md` B18「整轮失败:…不出回合状态行」
· `chat-panel-dev-design.md` 状态机 + 场景表「壳头「运行失败」收起 + 报错卡,
无回合状态行」;壳只有三态,「运行失败」是壳的词
· `chat-panel-next-review.md` B18 同条
· 交付稿「两边不重复:这一行只负责宣布『这轮到此为止』」
所以给 `hideRunStatus` 加第四条具名例外,按终态本身判而不是靠报错卡在不在。
判据绕开空流那一档:API 空回复也是 `runStatus:'failed'`,但它的状态词是
「没有输出」,由 `e2e/ui/api-empty-response.test.ts` 那条 P0 钉死。
顺带清掉两处已死的注释(纯注释,零行为):`.status-pill.is-error` 在
2026-08-27(`812e550ebe`)被无条件下线了,描述它「其他失败轮次保留 pill」的
那段英文注释和 `ChatPane` 里同一段话的拷贝都已不成立;后者改写成
`errorCardOwnerId` 今天唯一的活消费者是 `hideRunStatus`。
* fix(web): drop the session-mode chip from the Home composer
The Home composer footer carried a 「设计 ×」 mode chip (icon + mode name +
a clear ×) immediately left of the model switcher. The project composer
dropped the same chooser first (2026-08-19); Home was the last surface
still showing it, and every Home request defaulted past it to Design
anyway.
Removes the entry only. `ComposerModePicker` stays in the tree as a
dormant part with a header note saying when and why it was unhooked and
how to hook it back up; its i18n keys, `.composer-mode*` styles, its own
unit test and its `trackComposerSessionModeClick` analytics helper are
all left in place so restoring is a one-step change.
Behaviour is unchanged. `HomeView` still owns the `sessionMode` state
that feeds `conversationMode`, the automatic task profile and plugin
provenance; with nothing calling `setSessionMode` it simply stays on the
app default, `design`.
Pinned by a new `HomeView.mode-picker-removed.test.tsx` (absence at the
Home mount point, plus the payload invariant that Home still submits
`conversationMode: 'design'`).
* test(e2e): retarget the two Home specs that asserted the mode chip
These are the only two Playwright specs that positively asserted the
Home mode chip's presence; both follow the removal in the previous
commit.
`entry-chrome-flows` was a one-line presence check inside the default
prototype-scenario spec — flipped to `toHaveCount(0)`. That spec already
asserts `conversationMode === 'design'` from the request body, so the
routing it cared about is still covered.
`home-hero-rail`'s "home session mode toggle switches Ask planning
prompts away from design routing" drove the chip to pick Ask and checked
the request switched to `conversationMode: 'chat'` with no plugin. Home
has no surface that can select Ask any more, so that half is unreachable
rather than broken; the spec now pins the surviving truth — no picker on
screen, and every Home submission routes design with a plugin. Ask/Plan
routing itself is untouched and still covered on the project side, where
a conversation's stored session mode is what runs.
* fix(daemon): fill the codex command row in while the command runs
A codex `Bash` row stayed empty for the whole run and only showed its
output once the command exited. On the app-server transport codex does
send the child's output as it is produced
(`item/commandExecution/outputDelta`, confirmed against codex-cli 0.153.4
on 2026-09-08: one frame per write), but the normalizer had no case for
it, so every frame was counted as an unknown notification and dropped.
Routing the delta alone would not have been enough. The client retires an
early `tool_in_flight` row into the settled `tool_use` that shares its id,
and it does that by dropping every early row whose id already has a
settled one. Because `item/started` was forwarded to the `exec --json`
branch — which emits the SETTLED row — every later update would have been
discarded by the client without rendering, while every daemon-side test
stayed green. So the started frame now opens the row in its early form and
the settled pair comes from `item/completed`, where the output is final
anyway.
`startedAt` is the item's own `startedAtMs`, never the moment a delta
arrived, so the row's stopwatch does not restart on every chunk.
Output is bounded by the same two numbers the ACP bridge already uses
(2000 chars per event, 250ms between publications for one call): the cap
bounds one event, the interval bounds how many, and neither alone is
enough.
`exec --json` is untouched — that wire has no output-delta frame — so the
two transports now diverge by one event type. The parity test drops the
early form for the same reason it already drops `raw`, and the early rows
are asserted on their own instead.
* fix(web): resolve the composer plan tier through the approved workspace projection
The AMR model row in the composer popover read the exact billing SNAPSHOT to
decide a team workspace's plan tier. `workspaceBillingSummaryForContext` already
consults that snapshot first, but it also carries an approved, load-bearing
fallback: a TEAM-namespaced account tier may stand in when a team workspace has
no authorized snapshot. Reading the snapshot raw walked around it.
The daemon omits the `workspaceSnapshot` key whenever it cannot authorize one —
A answering 409 `billing_workspace_snapshot_unsupported` while `/wallet/balance`
still answers 200, a rolling deploy leaving an old API pod 404/405-ing the
route, or a local vela CLI predating `--workspace-id`. The tier then resolved to
null, `canUpgradeVelaPlan(null)` vetoed, and that veto landed BEFORE
`canReachWorkspaceBillingEntrance` was ever asked — so a team owner clicked a
plan-gated model and nothing happened at all.
Same for a team nobody has subscribed: B reports it as `billingState: 'free'`
with a null `planId`, and reading `planId` raw turned that positive "free" into
an unknown tier. The projection normalizes it to `'free'`, which is upgradeable.
The projection is a pure function of the response this component already holds,
so the corrected tier lands on the same render frame — no second async hop that
would paint the wrong identity first.
* fix(chat): keep next steps on the turn that earned them
OPEND-2764. A deck finished, the daemon emitted its three `next_steps`
suggestions, and the chat panel showed no next-step guidance at all — the user
had no way to keep refining what had just been delivered.
The memory card is a host-authored assistant message (`useMemoryWrittenCard`),
and extraction is queued after the child closes, so the card almost always
lands *after* the turn that produced the artifact. Next-step guidance was gated
on `isLast` — the last assistant message in the transcript, host cards
included — so the card displaced the delivering turn and the whole block, plus
the `suggestions` / `onSuggestion` props, was withheld.
"Last assistant message" answers three different questions in this panel and
none of them can stand in for the others: a question form asks whether anything
follows it (OPEND-2644), the brand assist card asks whether it is itself the
tail, and next-step guidance asks which turn is the conversation's current
landing point. Merging them turns the first two red. So this adds a criterion
rather than redefining the existing one: guidance belongs to the tail, and a
host-authored card is transparent to it — the turn keeps its suggestions, and
the next real turn still takes them away.
* fix(chat): move the question-form step progress next to the question it counts
OPEND-2641. In a stepped question form the `1/4` progress sat in the card
header, where `.qf-step-progress` and `.qf-picked` ("2 picked") each declared
`margin-inline-start: auto`. Two autos on one flex line split the free space in
half, so the progress parked in the middle of the header — detached from the
question it describes and competing with the selected-count label.
It now renders at the end of the current question row (`.qf-label`), after the
question text and after the required badge. The header keeps the card's own
name and card-level status only.
Layout rules that come with the move, both measured in headless Chrome rather
than assumed:
- `margin-inline-start: 6px` on the progress. The required badge and the
progress are adjacent siblings with no whitespace text node between them, so
at 0 the `1/4` sits flush against the badge's border (measured gap 0.0px).
- `overflow-wrap: break-word` on `.qf-label`. With a long unbreakable localized
compound at a 300px panel width the row overflowed its box (scrollWidth 412
vs clientWidth 298), pushing the question text out of the card. With it, 298.
Deliberately not added: `white-space: nowrap` on the progress. Measured in an
8px-wide box, `1/4` and `10/24` stay on one line even at `display: inline` +
`white-space: normal` — there is no break opportunity around the slash, so the
declaration would do nothing. `display: inline-block` is the actual guarantee.
`chat-mirror-cascade`'s property whitelist gains `margin-inline-*` and the two
physical longhands so the badge-separation assertion measures a real cascade
result instead of reading back `<unset>`.
* fix(web): drop the per-question subtitle from clarification cards
A clarification card question is its title, its required badge, and the
control the user answers with. The model-authored `help` subtitle sat
between the title and the control, pushing them apart and reading as the
card's own aside rather than as part of the question.
Stop rendering it. Dropping the node rather than hiding it also drops the
line box it held in the card body's vertical flow, so the title now sits
directly above its control with no leftover band of whitespace. `help`
stays on the parsed form shape, so existing and streamed forms keep
round-tripping unchanged.
Refs OPEND-2707
* fix(web): lead the queued row with steer and end it with delete
The three actions on a queued message read left to right as edit, delete,
steer — which puts the irreversible one in the middle of the sweep and
buries the action that acts on the run you are watching.
Reorder them into an escalation order: act-on-this-now first, edit second,
delete last. The leading slot keeps a stable landing point across both
faces of that button — "steer" while a run is interruptible, plain "send
now" when there is none — so the position does not move when the label
does. Nothing about what the three buttons do changes.
Refs OPEND-2715
* fix(web): collapse the fork origin hint into one divider line
Forking left two stacked blocks behind: a hairline divider labelled with
the source conversation's title, and a centred footnote under it. Two
rows, two ideas, for one fact.
Collapse them into the Codex shape — a branch glyph and a single line of
copy sitting in the middle of the divider. The source title stops being
rendered: a line that says "the context above came with you" is closer to
what the divider is for than one that reports an old title, and the title
is a click away in the conversation list. `forkedInto.title` stays on the
contract and in the database; only the rendering changes.
The label inherits the footnote's voice, not the title's: it is an
annotation of the line, the same class of chrome as the timestamp and the
copy button beside a message, so it takes the muted ink rather than brand
text. With one block instead of two there is no read order left to stagger,
so the footnote's 60ms entrance delay goes with it.
English copy is the reference image's, verbatim. The other locales keep
the strings they already had.
Refs OPEND-2714
* fix(web): move the notes chip's remove control onto its leading glyph
The notes chip carried a hidden remove button after the count. Hidden with
opacity, so it still held its place: measured in Chrome, the gap from the
count's right edge to the chip's inner edge was 31px against 9px on the
left — 5px of gap, 1px of margin and a 16px button that nobody could see.
The lopsided chip and the invisible button were the same fact.
Put the remove control where the speech bubble already is: one slot at the
head of the chip holding both marks, the bubble at rest and the remove
button when the pointer is over the chip or the keyboard has reached it.
The right side then has nothing left to reserve and closes to the padding,
so the chip is 9px on both sides and sized to what it actually shows.
Nothing about the action changes: same handler clearing every note at
once, same accessible name, no confirmation, and the chip still unmounts
when the last note goes. Only the affordance moved.
Measured in Chrome on the repo's own mirror page, before → after:
chip 104.9 → 85.9px wide, unchanged 28px tall, trailing gap 31 → 9px, and
the swap costs no reflow (identical box hovered and at rest).
Refs OPEND-2713
* fix(chat): stop a late host card from stealing the turn's streaming and retry
Two separate regressions with one physical cause. The host appends assistant
messages of its own — the memory card (`useMemoryWrittenCard`) and the brand
browser assist card — through `ProjectView.appendConversationMessage`. They
carry no runId / runStatus / startedAt / endedAt, and memory extraction is only
queued once the child process closes, so the card almost always lands *behind*
the turn it belongs to and displaces the physical tail. Two surfaces read
"the last assistant message" as their anchor and both were fooled.
1. API / BYOK streaming. `isAssistantMessageStreaming`'s final fallback
(`message.id !== lastAssistantId`) is the ONLY streaming source for a real
API-mode placeholder: that tier's optimistic message has no runId and no
runStatus (`runStatus: config.mode === 'daemon' ? 'running' : undefined`), so
it never reaches the `isActiveRunStatus` early return. When last turn's
memory card lands behind the streaming placeholder, `lastAssistantId` points
at the card, the fallback stops matching, and the running turn shows no
streaming indication at all — the panel just sits there.
OPEND-2745's reverse anchor could not see this: its placeholder is the tail,
with nothing behind it.
2. Retry after a failed turn. `retryableAssistantMessage` anchored on
`messages[messages.length - 1]`. A host card in that slot is not a terminal
failure, so `retryAssistant` went null and the whole recovery chain collapsed
with it — `runFailureUi`, [Retry] / [Resume], `errorCardOwnerId`. The turn
failed and the user had no way to push it forward.
Both fixes add a criterion instead of rewriting the existing one, following
OPEND-2764: "last assistant message" answers several different questions in this
panel and none can stand in for the others. Streaming keeps asking whether the
message is the tail, and now treats a host-authored card as transparent to that
question (`lastAssistantTurnId`). Retry keeps anchoring on the tail, and now
reads it through `trailingMessageIgnoringHostCards`. Neither makes the entry
sticky: a real next turn, or the user's own next message, still takes it away.
`ChatPane.error-card-raw-text-never-rendered.test.tsx` had pinned the defect
("retry is absent when the failed turn isn't last — and still is after this
change"); that PR scoped the vanishing entry out. It is now the invariant, plus
the genuine no-retry case (the user moved on).
* fix(web): keep the fork footnote on the message muted ink
Collapsing the fork hint into one line moved the footnote inside the
divider, and its colour moved with it — onto `.fork-sep span`, an
ancestor-scoped rule. `.fork-note` is still the class on that very
element, so anything rendering it on its own (the mirror gallery, the
muted-ink spec's bare fixture) was left with plain black text.
Give the label its own styling back. `.fork-note` owns what the footnote
is — icon beside text, 12px, message muted ink — and `.fork-sep span` is
left with only how that cell sits in the line: no stretching, 62% at most,
truncated rather than wrapped. The muted-ink custom property goes back on
`.fork-note` too, which is the point of "whoever uses it declares it": the
footnote has to stand up without an ancestor.
The colour also had to leave the ancestor rule to stay effective at all:
`.fork-sep span` outweighs `.fork-note`, so any declaration there silently
wins and the footnote's own can never apply.
Adds a companion assertion on the rendered node. The existing spec asks a
detached `.fork-note` what it is; that cannot see an ancestor rule
out-specifying it. Re-adding `color` to `.fork-sep span` proves the split:
the new assertion goes red, the detached one stays green.
Refs OPEND-2714
* fix(web): settle the fork footnote's layout against the one-line divider
The reverse-control spec pins five values on the footnote's rule. It was
written for the two-block divider, so collapsing that into one line put
two of them in question.
`display` goes back to the draft's `flex`. The footnote is an item of the
`.fork-sep` flex row, so its outer display is blockified either way and
`inline-flex` computed identically — a difference that bought nothing was
not worth diverging from the draft for.
`padding-bottom: 2px` goes, and stays gone. It was the breathing room
under a footnote that had its own row below the line. The footnote is now
a cell inside the line, and the line centres its items: two extra pixels
underneath grow the border box, so the text rides a pixel above the centre
the hairlines sit on. Keeping it would be the defect. The draft only ever
drew the two-block form, so this follows the precedent already set for the
one-line divider rather than any draft text.
The control keeps its teeth in both directions: the fifth value is
replaced by `align-items`, and a companion assertion pins the two-block
leftovers — `padding-bottom` and `justify-content` — as forbidden. Taking
a required value away reddens the first; back-filling a leftover reddens
the second.
Also corrects a stale premise in that spec's preamble: the mirror page's
missing `.fork-*` inlining, which the note describes in the present tense,
has since been fixed.
Refs OPEND-2714
* i18n(chat): take the product ruling on the fork footnote's Chinese copy
#2714 换掉了英文(`Context above came along — just keep going.` →
`Continued from chat`),但 18 个非英语语言包一字未动 —— 工单只给了英文,而
交付稿给的中文恰好就是原来那句,两边都有出处,不该由 agent 挑一边。
用户 2026-09-08 当面给定中文:「从上一个会话继续」。zh-TW 取同句繁体。
其余 17 个 locale **仍未动** —— 翻译属于产品文案,不自拟,需要逐条给。
顺带把跟着旧文案走的三处一起对上,否则它们会各自说一句产品里不存在的话:
· 陈列页第 38 格的样例正文与 `state` 描述;
· `AssistantMessage.tsx` / `ProjectView.tsx` 里论证「分界线为什么落在新会话」
时引用的那句脚注,以及两条测试的同款文件头注 —— 论证本身不变,新句子
「从上一个会话继续」同样只有站在新会话里回看才说得通。
裁决与四条遗留(其余 17 语、`.fork-sep span` 的省略号永不生效、生成产物
`mirror-exec.html` 仍停在两块式、无标题源会话不盖 `forkedInto`)记进
`specs/current/chat-panel-decisions-sheet.md`。`chat-panel-outro-audit.md`
里那三处旧文案是审计当时的记录,作为历史保留不改。
1564 lines
40 KiB
TypeScript
1564 lines
40 KiB
TypeScript
// Typed track* helpers for the v2 analytics schema. Each helper accepts a
|
|
// strongly typed props payload (from @open-design/contracts/analytics) and
|
|
// forwards it through the loosely typed `track()` from AnalyticsProvider.
|
|
// Keeping the event-name → prop-shape coupling in one place means call sites
|
|
// stay short and stay in lockstep with the daemon-side capture.
|
|
|
|
import type {
|
|
// page_view / surface_view
|
|
PageViewProps,
|
|
HelpPopoverSurfaceViewProps,
|
|
SettingsPopoverSurfaceViewProps,
|
|
NewProjectModalSurfaceViewProps,
|
|
PluginReplacementModalSurfaceViewProps,
|
|
PluginDetailModalSurfaceViewProps,
|
|
PluginImportModalSurfaceViewProps,
|
|
ProjectReferenceModalSurfaceViewProps,
|
|
FigmaHelpModalSurfaceViewProps,
|
|
DesignSystemsTemplatesModalSurfaceViewProps,
|
|
AssistantFeedbackReasonPanelSurfaceViewProps,
|
|
QuestionsFormSurfaceViewProps,
|
|
DeepSeekCampaignModalSurfaceViewProps,
|
|
GoPlanSunsetModalSurfaceViewProps,
|
|
DeepSeekCampaignBadgeSurfaceViewProps,
|
|
DeepSeekCampaignModelBenefitSurfaceViewProps,
|
|
// ui_click
|
|
HomeNavClickProps,
|
|
HelpPopoverClickProps,
|
|
HomeToolbarClickProps,
|
|
ExecutionSettingsPopoverClickProps,
|
|
SettingsPopoverClickProps,
|
|
HomeChatComposerClickProps,
|
|
UpdateIndicatorClickProps,
|
|
NewProjectModalTabClickProps,
|
|
NewProjectModalElementClickProps,
|
|
PluginReplacementModalClickProps,
|
|
PrivacyModalClickProps,
|
|
RecentProjectsClickProps,
|
|
HomeTemplatesClickProps,
|
|
HomeTemplatesDropdownClickProps,
|
|
ProjectsListControlsClickProps,
|
|
ProjectsListClickProps,
|
|
ProjectsMorePopoverClickProps,
|
|
AutomationsClickProps,
|
|
PluginsTopClickProps,
|
|
PluginsInstalledTabClickProps,
|
|
PluginsTemplatesDropdownClickProps,
|
|
PluginsAvailableTabClickProps,
|
|
PluginsSourcesTabClickProps,
|
|
PluginImportModalClickProps,
|
|
PluginDetailClickProps,
|
|
PluginLoopClickProps,
|
|
CommunityGalleryClickProps,
|
|
PluginDetailModalClickProps,
|
|
PluginDetailModalSharePopoverClickProps,
|
|
DesignSystemsTopClickProps,
|
|
DesignSystemsTemplateCardClickProps,
|
|
DesignSystemsTemplatesModalClickProps,
|
|
DesignSystemsTemplatesModalSharePopoverClickProps,
|
|
DesignSystemsCreateClickProps,
|
|
DesignSystemsPresetBrandPickerClickProps,
|
|
DesignSystemsPresetBrandPickerSurfaceViewProps,
|
|
DesignSystemEnrichClickProps,
|
|
DesignSystemEnrichResultProps,
|
|
DesignSystemEditClickProps,
|
|
IntegrationsTabClickProps,
|
|
IntegrationsMcpTabClickProps,
|
|
IntegrationsConnectorsTabClickProps,
|
|
IntegrationsSkillsTabClickProps,
|
|
IntegrationsUseEverywhereTabClickProps,
|
|
ChatPanelClickProps,
|
|
ComposerSessionModeClickProps,
|
|
ComposerBarClickProps,
|
|
DesignToolboxClickProps,
|
|
NextStepActionClickProps,
|
|
QuestionsFormClickProps,
|
|
RunFailedToastClickProps,
|
|
RunRecoveryActionClickProps,
|
|
AmrAuthResultProps,
|
|
AmrAuthStageProps,
|
|
AmrEntryClickProps,
|
|
PreviewRunStatusSurfaceViewProps,
|
|
DeepSeekCampaignModalClickProps,
|
|
GoPlanSunsetModalClickProps,
|
|
DeepSeekCampaignBadgeClickProps,
|
|
RunFailedToastSurfaceViewProps,
|
|
RunRecoveryActionSurfaceViewProps,
|
|
RunStartBlockedSurfaceViewProps,
|
|
HomeRecommendationClickProps,
|
|
HomeRecommendationSurfaceViewProps,
|
|
StudioOnboardingHintClickProps,
|
|
StudioOnboardingHintSurfaceViewProps,
|
|
ChatPanelResourcesPopoverClickProps,
|
|
ChatPanelMessageQueueClickProps,
|
|
FileManagerClickProps,
|
|
TabLauncherClickProps,
|
|
ReferenceBoardClickProps,
|
|
ReferenceBoardSurfaceViewProps,
|
|
ArtifactToolbarClickProps,
|
|
DrawToolbarClickProps,
|
|
TweaksPopoverClickProps,
|
|
CommentPopoverClickProps,
|
|
ArtifactHeaderClickProps,
|
|
HandoffClickProps,
|
|
PresentPopoverClickProps,
|
|
DeckViewerClickProps,
|
|
DeckViewerSurfaceViewProps,
|
|
ShareOptionPopoverClickProps,
|
|
FileVersionModalClickProps,
|
|
FileVersionModalSurfaceViewProps,
|
|
FileVersionRestoreResultProps,
|
|
AssistantFeedbackButtonClickProps,
|
|
AssistantFeedbackClickProps,
|
|
AssistantFeedbackReasonClickProps,
|
|
AssistantFeedbackReasonSubmitClickProps,
|
|
ConversationForkClickProps,
|
|
AssistantFeedbackReasonSubmitProps,
|
|
AssistantFeedbackReasonViewProps,
|
|
SettingsSidebarClickProps,
|
|
SettingsExecutionModeTabClickProps,
|
|
SettingsLocalCliClickProps,
|
|
SettingsByokProviderOptionClickProps,
|
|
SettingsByokFieldClickProps,
|
|
SettingsMediaProvidersClickProps,
|
|
SettingsConnectorsClickProps,
|
|
SettingsLanguageClickProps,
|
|
SettingsNotificationsClickProps,
|
|
SettingsPetsClickProps,
|
|
SettingsPrivacyClickProps,
|
|
SettingsDesignReviewClickProps,
|
|
SettingsExternalMcpClickProps,
|
|
// Result events
|
|
ProjectCreateResultProps,
|
|
PluginReplacementResultProps,
|
|
PluginImportResultProps,
|
|
RunCreatedProps,
|
|
RunFinishedProps,
|
|
FileUploadResultProps,
|
|
ContextLinkResultProps,
|
|
SpeakerNotesSaveResultProps,
|
|
ArtifactEditResultProps,
|
|
ArtifactExportResultProps,
|
|
ArtifactDeployResultProps,
|
|
ArtifactPublishResultProps,
|
|
SketchSaveResultProps,
|
|
SketchExportResultProps,
|
|
FeedbackSubmitResultProps,
|
|
ConversationForkResultProps,
|
|
SettingsViewProps,
|
|
LabsItemToggledProps,
|
|
SettingsCliTestResultProps,
|
|
SettingsByokModelsFetchResultProps,
|
|
SettingsByokTestResultProps,
|
|
SettingsConnectorAuthResultProps,
|
|
ByokPreflightBlockedProps,
|
|
OnboardingClickProps,
|
|
AgentDetectDiagnosticProps,
|
|
OnboardingRuntimeScanResultProps,
|
|
OnboardingCompleteResultProps,
|
|
OnboardingPromptPrefilledProps,
|
|
OnboardingFirstPromptSentProps,
|
|
OnboardingFirstGenerationCompletedProps,
|
|
DesignSystemSourceIngestResultProps,
|
|
DesignSystemCreateResultProps,
|
|
DesignSystemReviewResultProps,
|
|
DesignSystemStatusResultProps,
|
|
DesignSystemApplyResultProps,
|
|
UpdateIndicatorSurfaceViewProps,
|
|
UpdatePromptSurfaceViewProps,
|
|
UpdateCheckResultProps,
|
|
UpdateInstallResultProps,
|
|
WhatsNewPopupSurfaceViewProps,
|
|
WhatsNewPopupClickProps,
|
|
EntryNavigationClickProps,
|
|
AccountMenuClickProps,
|
|
WorkspaceSwitcherClickProps,
|
|
WorkspaceInviteClickProps,
|
|
ProjectCollectionClickProps,
|
|
CommunityTemplateClickProps,
|
|
ExtensionMarketplaceClickProps,
|
|
WorkspaceSurfaceViewProps,
|
|
WorkspaceSwitchResultProps,
|
|
WorkspaceInviteResultProps,
|
|
WorkspaceProjectActionResultProps,
|
|
WorkspaceSharedProjectOpenResultProps,
|
|
WorkspaceResourceActionResultProps,
|
|
ProjectCommentCreateResultProps,
|
|
} from '@open-design/contracts/analytics';
|
|
|
|
type TrackOptions = { requestId?: string; insertId?: string };
|
|
export type Track = (
|
|
event: string,
|
|
properties: Record<string, unknown>,
|
|
options?: TrackOptions,
|
|
) => void;
|
|
|
|
// Helper: forward a typed payload to the loose `track()` API. Centralized so
|
|
// every call site stays one-line.
|
|
import {
|
|
EXPERIENCE_SURVEY_ID,
|
|
EXPERIENCE_SURVEY_IMPROVEMENT_CHOICES,
|
|
EXPERIENCE_SURVEY_IMPROVEMENT_OTHER,
|
|
EXPERIENCE_SURVEY_QUESTION_IDS,
|
|
EXPERIENCE_SURVEY_QUESTION_TEXT,
|
|
EXPERIENCE_SURVEY_TRIGGER,
|
|
} from './experience-survey-contract';
|
|
|
|
function send<T extends object>(
|
|
track: Track,
|
|
event: string,
|
|
props: T,
|
|
options?: TrackOptions,
|
|
): void {
|
|
track(event, props as unknown as Record<string, unknown>, options);
|
|
}
|
|
|
|
// ---- page_view -----------------------------------------------------------
|
|
|
|
export function trackPageView(track: Track, props: PageViewProps): void {
|
|
send(track, 'page_view', props);
|
|
}
|
|
|
|
// ---- Workspace redesign -------------------------------------------------
|
|
|
|
export function trackEntryNavigationClick(track: Track, props: EntryNavigationClickProps): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackAccountMenuClick(track: Track, props: AccountMenuClickProps): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackWorkspaceSwitcherClick(track: Track, props: WorkspaceSwitcherClickProps): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackWorkspaceInviteClick(track: Track, props: WorkspaceInviteClickProps, options?: TrackOptions): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
export function trackProjectCollectionClick(track: Track, props: ProjectCollectionClickProps, options?: TrackOptions): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
export function trackCommunityTemplateClick(track: Track, props: CommunityTemplateClickProps): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackExtensionMarketplaceClick(track: Track, props: ExtensionMarketplaceClickProps): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackWorkspaceSurfaceView(track: Track, props: WorkspaceSurfaceViewProps): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackWorkspaceSwitchResult(track: Track, props: WorkspaceSwitchResultProps, options?: TrackOptions): void {
|
|
send(track, 'workspace_switch_result', props, options);
|
|
}
|
|
|
|
export function trackWorkspaceInviteResult(track: Track, props: WorkspaceInviteResultProps, options?: TrackOptions): void {
|
|
send(track, 'workspace_invite_result', props, options);
|
|
}
|
|
|
|
export function trackWorkspaceProjectActionResult(track: Track, props: WorkspaceProjectActionResultProps, options?: TrackOptions): void {
|
|
send(track, 'workspace_project_action_result', props, options);
|
|
}
|
|
|
|
export function trackWorkspaceSharedProjectOpenResult(track: Track, props: WorkspaceSharedProjectOpenResultProps, options?: TrackOptions): void {
|
|
send(track, 'workspace_shared_project_open_result', props, options);
|
|
}
|
|
|
|
export function trackWorkspaceResourceActionResult(track: Track, props: WorkspaceResourceActionResultProps, options?: TrackOptions): void {
|
|
send(track, 'workspace_resource_action_result', props, options);
|
|
}
|
|
|
|
export function trackProjectCommentCreateResult(track: Track, props: ProjectCommentCreateResultProps): void {
|
|
send(track, 'project_comment_create_result', props);
|
|
}
|
|
|
|
// ---- surface_view --------------------------------------------------------
|
|
|
|
export function trackHelpPopoverSurfaceView(
|
|
track: Track,
|
|
props: HelpPopoverSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackSettingsPopoverSurfaceView(
|
|
track: Track,
|
|
props: SettingsPopoverSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackNewProjectModalSurfaceView(
|
|
track: Track,
|
|
props: NewProjectModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackPluginReplacementModalSurfaceView(
|
|
track: Track,
|
|
props: PluginReplacementModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDesignSystemsTemplatesModalSurfaceView(
|
|
track: Track,
|
|
props: DesignSystemsTemplatesModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackPluginDetailModalSurfaceView(
|
|
track: Track,
|
|
props: PluginDetailModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackPluginImportModalSurfaceView(
|
|
track: Track,
|
|
props: PluginImportModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackProjectReferenceModalSurfaceView(
|
|
track: Track,
|
|
props: ProjectReferenceModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackFigmaHelpModalSurfaceView(
|
|
track: Track,
|
|
props: FigmaHelpModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackAssistantFeedbackReasonPanelSurfaceView(
|
|
track: Track,
|
|
props: AssistantFeedbackReasonPanelSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackRunFailedToastSurfaceView(
|
|
track: Track,
|
|
props: RunFailedToastSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackRunRecoveryActionSurfaceView(
|
|
track: Track,
|
|
props: RunRecoveryActionSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackRunStartBlockedSurfaceView(
|
|
track: Track,
|
|
props: RunStartBlockedSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackPreviewRunStatusSurfaceView(
|
|
track: Track,
|
|
props: PreviewRunStatusSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackQuestionsFormSurfaceView(
|
|
track: Track,
|
|
props: QuestionsFormSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackRunFailedToastGoAmrClick(
|
|
track: Track,
|
|
props: RunFailedToastClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackRunRecoveryActionClick(
|
|
track: Track,
|
|
props: RunRecoveryActionClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// Personalized first-run recommendation on Home (spec §7).
|
|
export function trackHomeRecommendationSurfaceView(
|
|
track: Track,
|
|
props: HomeRecommendationSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackHomeRecommendationClick(
|
|
track: Track,
|
|
props: HomeRecommendationClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// First-generation Studio hint (spec §8.3).
|
|
export function trackStudioOnboardingHintSurfaceView(
|
|
track: Track,
|
|
props: StudioOnboardingHintSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackStudioOnboardingHintClick(
|
|
track: Track,
|
|
props: StudioOnboardingHintClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackAmrEntryClick(
|
|
track: Track,
|
|
props: AmrEntryClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDeepSeekCampaignModalSurfaceView(
|
|
track: Track,
|
|
props: DeepSeekCampaignModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackGoPlanSunsetModalSurfaceView(
|
|
track: Track,
|
|
props: GoPlanSunsetModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDeepSeekCampaignBadgeSurfaceView(
|
|
track: Track,
|
|
props: DeepSeekCampaignBadgeSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDeepSeekCampaignModelBenefitSurfaceView(
|
|
track: Track,
|
|
props: DeepSeekCampaignModelBenefitSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDeepSeekCampaignModalClick(
|
|
track: Track,
|
|
props: DeepSeekCampaignModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackGoPlanSunsetModalClick(
|
|
track: Track,
|
|
props: GoPlanSunsetModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDeepSeekCampaignBadgeClick(
|
|
track: Track,
|
|
props: DeepSeekCampaignBadgeClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// Fired exactly once per AMR sign-in attempt when the login poll settles.
|
|
// Call sites go through analytics/amr-auth.ts, which owns the
|
|
// begin/resolve dedupe — do not call this wrapper directly from
|
|
// components, or concurrent pollers will double-report one attempt.
|
|
export function trackAmrAuthResult(
|
|
track: Track,
|
|
props: AmrAuthResultProps,
|
|
options?: TrackOptions,
|
|
): void {
|
|
send(track, 'amr_auth_result', props, options);
|
|
}
|
|
|
|
export function trackAmrAuthStage(
|
|
track: Track,
|
|
props: AmrAuthStageProps,
|
|
options?: TrackOptions,
|
|
): void {
|
|
send(track, 'amr_auth_stage', props, options);
|
|
}
|
|
|
|
// ---- ui_click (home) -----------------------------------------------------
|
|
|
|
export function trackHomeNavClick(
|
|
track: Track,
|
|
props: HomeNavClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHelpPopoverClick(
|
|
track: Track,
|
|
props: HelpPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHomeToolbarClick(
|
|
track: Track,
|
|
props: HomeToolbarClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackExecutionSettingsPopoverClick(
|
|
track: Track,
|
|
props: ExecutionSettingsPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsPopoverClick(
|
|
track: Track,
|
|
props: SettingsPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHomeChatComposerClick(
|
|
track: Track,
|
|
props: HomeChatComposerClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackUpdateIndicatorClick(
|
|
track: Track,
|
|
props: UpdateIndicatorClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackNewProjectModalTabClick(
|
|
track: Track,
|
|
props: NewProjectModalTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackNewProjectModalElementClick(
|
|
track: Track,
|
|
props: NewProjectModalElementClickProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
export function trackPluginReplacementModalClick(
|
|
track: Track,
|
|
props: PluginReplacementModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPrivacyModalClick(
|
|
track: Track,
|
|
props: PrivacyModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackRecentProjectsClick(
|
|
track: Track,
|
|
props: RecentProjectsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHomeTemplatesClick(
|
|
track: Track,
|
|
props: HomeTemplatesClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHomeTemplatesDropdownClick(
|
|
track: Track,
|
|
props: HomeTemplatesDropdownClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- ui_click (projects) -------------------------------------------------
|
|
|
|
export function trackProjectsListControlsClick(
|
|
track: Track,
|
|
props: ProjectsListControlsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackProjectsListClick(
|
|
track: Track,
|
|
props: ProjectsListClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackProjectsMorePopoverClick(
|
|
track: Track,
|
|
props: ProjectsMorePopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- ui_click (automations / plugins / design_systems / integrations) ----
|
|
|
|
export function trackAutomationsClick(
|
|
track: Track,
|
|
props: AutomationsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginsTopClick(
|
|
track: Track,
|
|
props: PluginsTopClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginsInstalledTabClick(
|
|
track: Track,
|
|
props: PluginsInstalledTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginsTemplatesDropdownClick(
|
|
track: Track,
|
|
props: PluginsTemplatesDropdownClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginsAvailableTabClick(
|
|
track: Track,
|
|
props: PluginsAvailableTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginsSourcesTabClick(
|
|
track: Track,
|
|
props: PluginsSourcesTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginImportModalClick(
|
|
track: Track,
|
|
props: PluginImportModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginDetailClick(
|
|
track: Track,
|
|
props: PluginDetailClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginLoopClick(
|
|
track: Track,
|
|
props: PluginLoopClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackCommunityGalleryClick(
|
|
track: Track,
|
|
props: CommunityGalleryClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginDetailModalClick(
|
|
track: Track,
|
|
props: PluginDetailModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPluginDetailModalSharePopoverClick(
|
|
track: Track,
|
|
props: PluginDetailModalSharePopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsTopClick(
|
|
track: Track,
|
|
props: DesignSystemsTopClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsTemplateCardClick(
|
|
track: Track,
|
|
props: DesignSystemsTemplateCardClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsTemplatesModalClick(
|
|
track: Track,
|
|
props: DesignSystemsTemplatesModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsTemplatesModalSharePopoverClick(
|
|
track: Track,
|
|
props: DesignSystemsTemplatesModalSharePopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsCreateClick(
|
|
track: Track,
|
|
props: DesignSystemsCreateClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsPresetBrandPickerClick(
|
|
track: Track,
|
|
props: DesignSystemsPresetBrandPickerClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemsPresetBrandPickerSurfaceView(
|
|
track: Track,
|
|
props: DesignSystemsPresetBrandPickerSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDesignSystemEnrichClick(
|
|
track: Track,
|
|
props: DesignSystemEnrichClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignSystemEnrichResult(
|
|
track: Track,
|
|
props: DesignSystemEnrichResultProps,
|
|
): void {
|
|
send(track, 'design_system_enrich_result', props);
|
|
}
|
|
|
|
export function trackDesignSystemEditClick(
|
|
track: Track,
|
|
props: DesignSystemEditClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackIntegrationsTabClick(
|
|
track: Track,
|
|
props: IntegrationsTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackIntegrationsMcpTabClick(
|
|
track: Track,
|
|
props: IntegrationsMcpTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackIntegrationsConnectorsTabClick(
|
|
track: Track,
|
|
props: IntegrationsConnectorsTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackIntegrationsSkillsTabClick(
|
|
track: Track,
|
|
props: IntegrationsSkillsTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackIntegrationsUseEverywhereTabClick(
|
|
track: Track,
|
|
props: IntegrationsUseEverywhereTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- ui_click (chat panel) -----------------------------------------------
|
|
|
|
export function trackChatPanelClick(
|
|
track: Track,
|
|
props: ChatPanelClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// Dormant with `ComposerModePicker` (see that file's header): both composers
|
|
// have dropped the mode chip, so nothing calls this today. Kept so the picker
|
|
// can be restored in one step — do NOT delete it as dead code.
|
|
export function trackComposerSessionModeClick(
|
|
track: Track,
|
|
props: ComposerSessionModeClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDesignToolboxClick(
|
|
track: Track,
|
|
props: DesignToolboxClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSketchSaveResult(
|
|
track: Track,
|
|
props: SketchSaveResultProps,
|
|
): void {
|
|
send(track, 'sketch_save_result', props);
|
|
}
|
|
|
|
export function trackSketchExportResult(
|
|
track: Track,
|
|
props: SketchExportResultProps,
|
|
): void {
|
|
send(track, 'sketch_export_result', props);
|
|
}
|
|
|
|
export function trackComposerBarClick(
|
|
track: Track,
|
|
props: ComposerBarClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackNextStepActionClick(
|
|
track: Track,
|
|
props: NextStepActionClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackQuestionsFormClick(
|
|
track: Track,
|
|
props: QuestionsFormClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackChatPanelResourcesPopoverClick(
|
|
track: Track,
|
|
props: ChatPanelResourcesPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackMessageQueueClick(
|
|
track: Track,
|
|
props: ChatPanelMessageQueueClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- ui_click (file manager / artifact) ----------------------------------
|
|
|
|
export function trackFileManagerClick(
|
|
track: Track,
|
|
props: FileManagerClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackTabLauncherClick(
|
|
track: Track,
|
|
props: TabLauncherClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackReferenceBoardSurfaceView(
|
|
track: Track,
|
|
props: ReferenceBoardSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackReferenceBoardClick(
|
|
track: Track,
|
|
props: ReferenceBoardClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackArtifactToolbarClick(
|
|
track: Track,
|
|
props: ArtifactToolbarClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDrawToolbarClick(
|
|
track: Track,
|
|
props: DrawToolbarClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackTweaksPopoverClick(
|
|
track: Track,
|
|
props: TweaksPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackCommentPopoverClick(
|
|
track: Track,
|
|
props: CommentPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackArtifactHeaderClick(
|
|
track: Track,
|
|
props: ArtifactHeaderClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackHandoffClick(
|
|
track: Track,
|
|
props: HandoffClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackPresentPopoverClick(
|
|
track: Track,
|
|
props: PresentPopoverClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackDeckViewerSurfaceView(
|
|
track: Track,
|
|
props: DeckViewerSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackDeckViewerClick(
|
|
track: Track,
|
|
props: DeckViewerClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSpeakerNotesSaveResult(
|
|
track: Track,
|
|
props: SpeakerNotesSaveResultProps,
|
|
): void {
|
|
send(track, 'speaker_notes_save_result', props);
|
|
}
|
|
|
|
export function trackShareOptionPopoverClick(
|
|
track: Track,
|
|
props: ShareOptionPopoverClickProps,
|
|
options?: { requestId: string },
|
|
): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
export function trackFileVersionModalClick(
|
|
track: Track,
|
|
props: FileVersionModalClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackFileVersionModalSurfaceView(
|
|
track: Track,
|
|
props: FileVersionModalSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
// ---- ui_click (feedback) -------------------------------------------------
|
|
|
|
export function trackAssistantFeedbackButtonClick(
|
|
track: Track,
|
|
props: AssistantFeedbackButtonClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackConversationForkClick(
|
|
track: Track,
|
|
props: ConversationForkClickProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
export function trackAssistantFeedbackReasonSubmitClick(
|
|
track: Track,
|
|
props: AssistantFeedbackReasonSubmitClickProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'ui_click', props, options);
|
|
}
|
|
|
|
// ---- ui_click (settings) -------------------------------------------------
|
|
|
|
export function trackSettingsSidebarClick(
|
|
track: Track,
|
|
props: SettingsSidebarClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsExecutionModeTabClick(
|
|
track: Track,
|
|
props: SettingsExecutionModeTabClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsLocalCliClick(
|
|
track: Track,
|
|
props: SettingsLocalCliClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsByokProviderOptionClick(
|
|
track: Track,
|
|
props: SettingsByokProviderOptionClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsByokFieldClick(
|
|
track: Track,
|
|
props: SettingsByokFieldClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsMediaProvidersClick(
|
|
track: Track,
|
|
props: SettingsMediaProvidersClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsConnectorsClick(
|
|
track: Track,
|
|
props: SettingsConnectorsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsLanguageClick(
|
|
track: Track,
|
|
props: SettingsLanguageClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsNotificationsClick(
|
|
track: Track,
|
|
props: SettingsNotificationsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsPetsClick(
|
|
track: Track,
|
|
props: SettingsPetsClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsPrivacyClick(
|
|
track: Track,
|
|
props: SettingsPrivacyClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsDesignReviewClick(
|
|
track: Track,
|
|
props: SettingsDesignReviewClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackSettingsExternalMcpClick(
|
|
track: Track,
|
|
props: SettingsExternalMcpClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- Result events -------------------------------------------------------
|
|
|
|
export function trackProjectCreateResult(
|
|
track: Track,
|
|
props: ProjectCreateResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'project_create_result', props, options);
|
|
}
|
|
|
|
export function trackPluginReplacementResult(
|
|
track: Track,
|
|
props: PluginReplacementResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'plugin_replacement_result', props, options);
|
|
}
|
|
|
|
export function trackPluginImportResult(
|
|
track: Track,
|
|
props: PluginImportResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'plugin_import_result', props, options);
|
|
}
|
|
|
|
export function trackRunCreated(
|
|
track: Track,
|
|
props: RunCreatedProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'run_created', props, options);
|
|
}
|
|
|
|
export function trackRunFinished(
|
|
track: Track,
|
|
props: RunFinishedProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'run_finished', props, options);
|
|
}
|
|
|
|
export function trackFileUploadResult(
|
|
track: Track,
|
|
props: FileUploadResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'file_upload_result', props, options);
|
|
}
|
|
|
|
export function trackContextLinkResult(
|
|
track: Track,
|
|
props: ContextLinkResultProps,
|
|
): void {
|
|
send(track, 'context_link_result', props);
|
|
}
|
|
|
|
export function trackArtifactExportResult(
|
|
track: Track,
|
|
props: ArtifactExportResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'artifact_export_result', props, options);
|
|
}
|
|
|
|
export function trackArtifactEditResult(
|
|
track: Track,
|
|
props: ArtifactEditResultProps,
|
|
): void {
|
|
send(track, 'artifact_edit_result', props);
|
|
}
|
|
|
|
export function trackArtifactDeployResult(
|
|
track: Track,
|
|
props: ArtifactDeployResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'artifact_deploy_result', props, options);
|
|
}
|
|
|
|
export function trackArtifactPublishResult(
|
|
track: Track,
|
|
props: ArtifactPublishResultProps,
|
|
): void {
|
|
send(track, 'artifact_publish_result', props);
|
|
}
|
|
|
|
export function trackFileVersionRestoreResult(
|
|
track: Track,
|
|
props: FileVersionRestoreResultProps,
|
|
): void {
|
|
send(track, 'file_version_restore_result', props);
|
|
}
|
|
|
|
export function trackFeedbackSubmitResult(
|
|
track: Track,
|
|
props: FeedbackSubmitResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'feedback_submit_result', props, options);
|
|
}
|
|
|
|
export function trackConversationForkResult(
|
|
track: Track,
|
|
props: ConversationForkResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'conversation_fork_result', props, options);
|
|
}
|
|
|
|
// ---- Settings view + test/auth result events -----------------------------
|
|
|
|
export function trackSettingsView(
|
|
track: Track,
|
|
props: SettingsViewProps,
|
|
): void {
|
|
send(track, 'settings_view', props);
|
|
}
|
|
|
|
// ---- Labs ----------------------------------------------------------------
|
|
|
|
/**
|
|
* Fires after the preference is persisted, not on click.
|
|
*
|
|
* The event asserts "this install now prefers X". A failed write rolls the
|
|
* switch back, so reporting the click would assert something that is not true
|
|
* of the machine. Losing the rare failed toggle is the cheaper error.
|
|
*/
|
|
export function trackLabsItemToggled(track: Track, props: LabsItemToggledProps): void {
|
|
send(track, 'labs_item_toggled', props);
|
|
}
|
|
|
|
export function trackSettingsCliTestResult(
|
|
track: Track,
|
|
props: SettingsCliTestResultProps,
|
|
): void {
|
|
send(track, 'settings_cli_test_result', props);
|
|
}
|
|
|
|
export function trackSettingsByokTestResult(
|
|
track: Track,
|
|
props: SettingsByokTestResultProps,
|
|
): void {
|
|
send(track, 'settings_byok_test_result', props);
|
|
}
|
|
|
|
export function trackSettingsByokModelsFetchResult(
|
|
track: Track,
|
|
props: SettingsByokModelsFetchResultProps,
|
|
): void {
|
|
send(track, 'settings_byok_models_fetch_result', props);
|
|
}
|
|
|
|
export function trackByokPreflightBlocked(
|
|
track: Track,
|
|
props: ByokPreflightBlockedProps,
|
|
): void {
|
|
send(track, 'byok_preflight_blocked', props);
|
|
}
|
|
|
|
export function trackSettingsConnectorAuthResult(
|
|
track: Track,
|
|
props: SettingsConnectorAuthResultProps,
|
|
): void {
|
|
send(track, 'settings_connector_auth_result', props);
|
|
}
|
|
|
|
export function trackAssistantFeedbackClick(
|
|
track: Track,
|
|
props: AssistantFeedbackClickProps,
|
|
) {
|
|
track(
|
|
'assistant_feedback_click',
|
|
props as unknown as Record<string, unknown>,
|
|
);
|
|
}
|
|
|
|
export function trackAssistantFeedbackReasonView(
|
|
track: Track,
|
|
props: AssistantFeedbackReasonViewProps,
|
|
) {
|
|
track(
|
|
'assistant_feedback_reason_view',
|
|
props as unknown as Record<string, unknown>,
|
|
);
|
|
}
|
|
|
|
export function trackAssistantFeedbackReasonClick(
|
|
track: Track,
|
|
props: AssistantFeedbackReasonClickProps,
|
|
options?: { requestId: string },
|
|
) {
|
|
track(
|
|
'assistant_feedback_reason_click',
|
|
props as unknown as Record<string, unknown>,
|
|
options,
|
|
);
|
|
}
|
|
|
|
export function trackAssistantFeedbackReasonSubmit(
|
|
track: Track,
|
|
props: AssistantFeedbackReasonSubmitProps,
|
|
options?: { requestId: string },
|
|
) {
|
|
track(
|
|
'assistant_feedback_reason_submit',
|
|
props as unknown as Record<string, unknown>,
|
|
options,
|
|
);
|
|
}
|
|
|
|
// ---- Onboarding ---------------------------------------------------------
|
|
//
|
|
// `trackOnboardingClick` is the catch-all for the welcome flow's
|
|
// runtime-pick / about-you / continue / skip / back buttons. The same
|
|
// shape still supports historical and future design-system intake
|
|
// clicks; the discriminator combo (area + element + action) narrows
|
|
// the row down inside PostHog so the dashboard can split each step's
|
|
// funnel cleanly without a separate event name per button. Lifecycle
|
|
// events that don't fit a click — CLI scan finishing, onboarding
|
|
// wrapping up — get their own `onboarding_*_result` shape.
|
|
|
|
export function trackOnboardingClick(
|
|
track: Track,
|
|
props: OnboardingClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
export function trackAgentDetectDiagnostic(
|
|
track: Track,
|
|
props: AgentDetectDiagnosticProps,
|
|
): void {
|
|
send(track, 'agent_detect_diagnostic', props);
|
|
}
|
|
|
|
export function trackOnboardingRuntimeScanResult(
|
|
track: Track,
|
|
props: OnboardingRuntimeScanResultProps,
|
|
): void {
|
|
send(track, 'onboarding_runtime_scan_result', props);
|
|
}
|
|
|
|
export function trackOnboardingCompleteResult(
|
|
track: Track,
|
|
props: OnboardingCompleteResultProps,
|
|
): void {
|
|
send(track, 'onboarding_complete_result', props);
|
|
}
|
|
|
|
// First-generation funnel (spec §11.1).
|
|
export function trackOnboardingPromptPrefilled(
|
|
track: Track,
|
|
props: OnboardingPromptPrefilledProps,
|
|
): void {
|
|
send(track, 'onboarding_prompt_prefilled', props);
|
|
}
|
|
|
|
export function trackOnboardingFirstPromptSent(
|
|
track: Track,
|
|
props: OnboardingFirstPromptSentProps,
|
|
): void {
|
|
send(track, 'onboarding_first_prompt_sent', props);
|
|
}
|
|
|
|
export function trackOnboardingFirstGenerationCompleted(
|
|
track: Track,
|
|
props: OnboardingFirstGenerationCompletedProps,
|
|
): void {
|
|
send(track, 'onboarding_first_generation_completed', props);
|
|
}
|
|
|
|
// ---- Design-system lifecycle ---------------------------------------------
|
|
//
|
|
// `trackDesignSystem*Result` cover the five lifecycle moments in the
|
|
// DS funnel: source intake, create, review, status changes, picker
|
|
// apply. Page_views / clicks inside DS surfaces continue to reuse the
|
|
// generic `page_view` / `ui_click` helpers with the DS page enum.
|
|
|
|
export function trackDesignSystemSourceIngestResult(
|
|
track: Track,
|
|
props: DesignSystemSourceIngestResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'design_system_source_ingest_result', props, options);
|
|
}
|
|
|
|
export function trackDesignSystemCreateResult(
|
|
track: Track,
|
|
props: DesignSystemCreateResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'design_system_create_result', props, options);
|
|
}
|
|
|
|
export function trackDesignSystemReviewResult(
|
|
track: Track,
|
|
props: DesignSystemReviewResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'design_system_review_result', props, options);
|
|
}
|
|
|
|
export function trackDesignSystemStatusResult(
|
|
track: Track,
|
|
props: DesignSystemStatusResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'design_system_status_result', props, options);
|
|
}
|
|
|
|
export function trackDesignSystemApplyResult(
|
|
track: Track,
|
|
props: DesignSystemApplyResultProps,
|
|
options?: { requestId?: string },
|
|
): void {
|
|
send(track, 'design_system_apply_result', props, options);
|
|
}
|
|
|
|
// ---- Update indicator / prompt ------------------------------------------
|
|
|
|
export function trackUpdateIndicatorSurfaceView(
|
|
track: Track,
|
|
props: UpdateIndicatorSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackUpdatePromptSurfaceView(
|
|
track: Track,
|
|
props: UpdatePromptSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackUpdateInstallResult(
|
|
track: Track,
|
|
props: UpdateInstallResultProps,
|
|
): void {
|
|
send(track, 'update_install_result', props);
|
|
}
|
|
|
|
export function trackUpdateCheckResult(
|
|
track: Track,
|
|
props: UpdateCheckResultProps,
|
|
): void {
|
|
send(track, 'update_check_result', props);
|
|
}
|
|
|
|
// ---- Post-update "what's new" card ---------------------------------------
|
|
|
|
export function trackWhatsNewPopupSurfaceView(
|
|
track: Track,
|
|
props: WhatsNewPopupSurfaceViewProps,
|
|
): void {
|
|
send(track, 'surface_view', props);
|
|
}
|
|
|
|
export function trackWhatsNewPopupClick(
|
|
track: Track,
|
|
props: WhatsNewPopupClickProps,
|
|
): void {
|
|
send(track, 'ui_click', props);
|
|
}
|
|
|
|
// ---- experience survey ---------------------------------------------------
|
|
// The experience survey is `type: api` in PostHog: PostHog stores and analyses
|
|
// the responses while `ExperienceSurvey` renders the card. That makes the
|
|
// client responsible for the three reserved event names PostHog's survey
|
|
// analytics reads, which is the only place in this app that emits reserved
|
|
// PostHog events rather than the v2 schema's own.
|
|
|
|
export function trackExperienceSurveyShown(track: Track): void {
|
|
send(track, 'survey shown', {
|
|
$survey_id: EXPERIENCE_SURVEY_ID,
|
|
trigger: EXPERIENCE_SURVEY_TRIGGER,
|
|
});
|
|
}
|
|
|
|
export function trackExperienceSurveyDismissed(track: Track): void {
|
|
send(track, 'survey dismissed', {
|
|
$survey_id: EXPERIENCE_SURVEY_ID,
|
|
trigger: EXPERIENCE_SURVEY_TRIGGER,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reports a finished response. A skipped follow-up is omitted rather than sent
|
|
* as null, so PostHog's per-question response counts stay honest about how
|
|
* many people actually answered it.
|
|
*/
|
|
export function trackExperienceSurveySent(
|
|
track: Track,
|
|
answers: { recommendation: number; improvement?: number; improvementOther?: string },
|
|
): void {
|
|
const ids = EXPERIENCE_SURVEY_QUESTION_IDS;
|
|
const text = EXPERIENCE_SURVEY_QUESTION_TEXT;
|
|
const answered: Array<{ id: string; question: string; response: string | number }> = [];
|
|
const responses: Record<string, string | number> = {};
|
|
|
|
const add = (id: string, question: string, response: string | number) => {
|
|
answered.push({ id, question, response });
|
|
responses[`$survey_response_${id}`] = response;
|
|
};
|
|
|
|
add(ids.recommendation, text.recommendation, answers.recommendation);
|
|
if (typeof answers.improvementOther === 'string') {
|
|
// PostHog's open-choice convention: the response is what they typed. An
|
|
// empty field still reports the choice itself, so "none of these fit"
|
|
// survives instead of looking like the question was skipped.
|
|
add(
|
|
ids.improvement,
|
|
text.improvement,
|
|
answers.improvementOther.trim() || EXPERIENCE_SURVEY_IMPROVEMENT_OTHER,
|
|
);
|
|
} else if (typeof answers.improvement === 'number') {
|
|
const choice = EXPERIENCE_SURVEY_IMPROVEMENT_CHOICES[answers.improvement];
|
|
if (choice) add(ids.improvement, text.improvement, choice);
|
|
}
|
|
|
|
send(track, 'survey sent', {
|
|
$survey_id: EXPERIENCE_SURVEY_ID,
|
|
trigger: EXPERIENCE_SURVEY_TRIGGER,
|
|
$survey_questions: answered,
|
|
...responses,
|
|
});
|
|
}
|