Skip to content

Commit ba7d56e

Browse files
feat(api): add analytics-scoped public API and clean up analytics data access (#1119)
* feat(api): add analytics-scoped public API and clean up analytics data access Publishes all 30 internal analytics procedures under a new `analytics` token scope at /v1/analytics/*, closing the gap where analytics tokens could only reach error logs. Since apps/mcp-server derives its tool list from /public-spec.json, this also exposes the endpoints as MCP tools with no MCP-side work. Also moves inline db.* queries out of packages/analytics services and into their matching repositories, fixing the same layering violation the broader data-access cleanup targets, in the domain this PR already touches. * fix(analytics): scope broadcast stats to workspace and close analytics public-API review gaps The new analytics public API (#1119) made broadcastStats reachable by API token for the first time, exposing a pre-existing IDOR: getBatchStats/getContacts never filtered on workspaceId, so a workspace-A token could read workspace-B's broadcast delivery/seen/ click/failure counts and contact lists via a guessable broadcastId. Both now join Broadcast and scope on workspaceId. resetStatsSession had a related but lower-severity issue: it inserted a FlowAnalyticsSession row for any flowId without verifying it belongs to the caller's workspace, letting a foreign flowId write an orphan row into the caller's workspace on every call. It now no-ops when the flow isn't found in-scope. Also addresses the rest of the PR #1119 review: - extend the public-spec workspaceId leak guard to request schemas (parameters + body), not just responses - replace withPublicPaging's `as never` cast with shape destructuring so a schema missing page/perPage fails at compile time instead of crashing every public route at module load - de-duplicate findContactInboxesWithContact into BaseRepository - reuse existing @chatbotx.io/analytics/schemas response schemas instead of hand-copying four of them in schema/public.ts - finish the db.* cleanup: add LinkStatsRepository.insertStats() and move magic-link/ref-link click inserts off direct db.insert - swap sql.raw for sql.identifier in sequence-stats.repository.ts - tighten the refLinkContacts PII test to the full allow-list form * chore: change contact update method to contact repository * chore: fix public-spec test
1 parent 870e35a commit ba7d56e

33 files changed

Lines changed: 2724 additions & 399 deletions

apps/builder/__tests__/__snapshots__/public-spec-operations.test.ts.snap

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,156 @@ exports[`public API spec — operation naming guard > operation list (operationI
5757
"operationId": "aiTriggers.update",
5858
"path": "/v1/ai-triggers/{id}",
5959
},
60+
{
61+
"method": "GET",
62+
"operationId": "analytics.activeContactsCount",
63+
"path": "/v1/analytics/active-contacts-count",
64+
},
65+
{
66+
"method": "GET",
67+
"operationId": "analytics.blockedContactsCount",
68+
"path": "/v1/analytics/blocked-contacts-count",
69+
},
70+
{
71+
"method": "GET",
72+
"operationId": "analytics.blockedContactsPerDay",
73+
"path": "/v1/analytics/blocked-contacts-per-day",
74+
},
75+
{
76+
"method": "GET",
77+
"operationId": "analytics.botMessagesAiProviders",
78+
"path": "/v1/analytics/bot-messages-ai-providers",
79+
},
80+
{
81+
"method": "GET",
82+
"operationId": "analytics.botMessagesByResult",
83+
"path": "/v1/analytics/bot-messages-by-result",
84+
},
85+
{
86+
"method": "GET",
87+
"operationId": "analytics.botMessagesNoResponse",
88+
"path": "/v1/analytics/bot-messages-no-response",
89+
},
90+
{
91+
"method": "GET",
92+
"operationId": "analytics.botMessagesWithResponse",
93+
"path": "/v1/analytics/bot-messages-with-response",
94+
},
95+
{
96+
"method": "GET",
97+
"operationId": "analytics.broadcastStats",
98+
"path": "/v1/analytics/broadcasts/{broadcastId}/stats",
99+
},
100+
{
101+
"method": "GET",
102+
"operationId": "analytics.contactCountsPerDay",
103+
"path": "/v1/analytics/contact-counts-per-day",
104+
},
105+
{
106+
"method": "GET",
107+
"operationId": "analytics.contactsByDimension",
108+
"path": "/v1/analytics/contacts-by-dimension",
109+
},
110+
{
111+
"method": "GET",
112+
"operationId": "analytics.contactsCount",
113+
"path": "/v1/analytics/contacts-count",
114+
},
115+
{
116+
"method": "GET",
117+
"operationId": "analytics.conversationArchived",
118+
"path": "/v1/analytics/conversation-archived",
119+
},
120+
{
121+
"method": "GET",
122+
"operationId": "analytics.conversationAssigned",
123+
"path": "/v1/analytics/conversation-assigned",
124+
},
125+
{
126+
"method": "GET",
127+
"operationId": "analytics.conversationAssignedByAdmin",
128+
"path": "/v1/analytics/conversation-assigned-by-admin",
129+
},
130+
{
131+
"method": "GET",
132+
"operationId": "analytics.conversationFollowUps",
133+
"path": "/v1/analytics/conversation-followups",
134+
},
135+
{
136+
"method": "GET",
137+
"operationId": "analytics.conversationHandoffs",
138+
"path": "/v1/analytics/conversation-handoffs",
139+
},
140+
{
141+
"method": "GET",
142+
"operationId": "analytics.flowStats",
143+
"path": "/v1/analytics/flows/{flowId}",
144+
},
145+
{
146+
"method": "GET",
147+
"operationId": "analytics.humanAgentStats",
148+
"path": "/v1/analytics/human-agent-stats",
149+
},
150+
{
151+
"method": "GET",
152+
"operationId": "analytics.macActiveContactCount",
153+
"path": "/v1/analytics/mac/active-count",
154+
},
155+
{
156+
"method": "GET",
157+
"operationId": "analytics.magicLinkContacts",
158+
"path": "/v1/analytics/magic-links/contacts",
159+
},
160+
{
161+
"method": "GET",
162+
"operationId": "analytics.magicLinkStats",
163+
"path": "/v1/analytics/magic-links/stats",
164+
},
165+
{
166+
"method": "GET",
167+
"operationId": "analytics.messagesByAdmin",
168+
"path": "/v1/analytics/messages-by-admin",
169+
},
170+
{
171+
"method": "GET",
172+
"operationId": "analytics.messagesBySender",
173+
"path": "/v1/analytics/messages-by-sender",
174+
},
175+
{
176+
"method": "GET",
177+
"operationId": "analytics.newContactCountsPerDay",
178+
"path": "/v1/analytics/new-contact-counts-per-day",
179+
},
180+
{
181+
"method": "GET",
182+
"operationId": "analytics.newContactsCount",
183+
"path": "/v1/analytics/new-contacts-count",
184+
},
185+
{
186+
"method": "GET",
187+
"operationId": "analytics.refLinkContacts",
188+
"path": "/v1/analytics/ref-links/contacts",
189+
},
190+
{
191+
"method": "GET",
192+
"operationId": "analytics.refLinkStats",
193+
"path": "/v1/analytics/ref-links/stats",
194+
},
195+
{
196+
"method": "DELETE",
197+
"operationId": "analytics.resetFlowStats",
198+
"path": "/v1/analytics/flows/{flowId}",
199+
},
200+
{
201+
"method": "GET",
202+
"operationId": "analytics.sequenceStepStats",
203+
"path": "/v1/analytics/sequences/{sequenceId}/steps/{stepId}/stats",
204+
},
205+
{
206+
"method": "GET",
207+
"operationId": "analytics.uniqueConversationsByAdmin",
208+
"path": "/v1/analytics/unique-conversations-by-admin",
209+
},
60210
{
61211
"method": "PUT",
62212
"operationId": "botFields.bulkUpdate",

0 commit comments

Comments
 (0)