@carlonicora/nextjs-jsonapi 3.24.2 → 3.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
  2. package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
  3. package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-4EIKMNY3.js} +31 -21
  4. package/dist/BlockNoteEditor-4EIKMNY3.js.map +1 -0
  5. package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-AXYVDDXM.mjs} +16 -6
  6. package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-AXYVDDXM.mjs.map} +1 -1
  7. package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
  8. package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
  9. package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
  10. package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
  11. package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
  12. package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
  13. package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
  14. package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
  15. package/dist/analytics.modules-CfczU12P.d.ts +25 -0
  16. package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
  17. package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
  18. package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
  19. package/dist/billing/index.js +402 -402
  20. package/dist/billing/index.mjs +4 -4
  21. package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
  22. package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
  23. package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
  24. package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
  25. package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
  26. package/dist/chunk-7B4E42R5.mjs +280 -0
  27. package/dist/chunk-7B4E42R5.mjs.map +1 -0
  28. package/dist/{chunk-2XCUAHQT.js → chunk-ABVTYQIJ.js} +1563 -1336
  29. package/dist/chunk-ABVTYQIJ.js.map +1 -0
  30. package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
  31. package/dist/chunk-CAKOFJZD.js.map +1 -0
  32. package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
  33. package/dist/chunk-E3I4MT53.mjs.map +1 -0
  34. package/dist/chunk-GHFMKH3M.js +280 -0
  35. package/dist/chunk-GHFMKH3M.js.map +1 -0
  36. package/dist/chunk-GKDHYB2R.js +110 -0
  37. package/dist/chunk-GKDHYB2R.js.map +1 -0
  38. package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
  39. package/dist/{chunk-4O7R2Z2O.mjs → chunk-QWZAGQWJ.mjs} +2175 -1948
  40. package/dist/chunk-QWZAGQWJ.mjs.map +1 -0
  41. package/dist/chunk-U65JPKO3.mjs +110 -0
  42. package/dist/chunk-U65JPKO3.mjs.map +1 -0
  43. package/dist/client/index.d.mts +6 -5
  44. package/dist/client/index.d.ts +6 -5
  45. package/dist/client/index.js +5 -5
  46. package/dist/client/index.js.map +1 -1
  47. package/dist/client/index.mjs +4 -4
  48. package/dist/components/index.d.mts +33 -10
  49. package/dist/components/index.d.ts +33 -10
  50. package/dist/components/index.js +7 -5
  51. package/dist/components/index.js.map +1 -1
  52. package/dist/components/index.mjs +6 -4
  53. package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
  54. package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
  55. package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
  56. package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
  57. package/dist/contexts/index.d.mts +5 -4
  58. package/dist/contexts/index.d.ts +5 -4
  59. package/dist/contexts/index.js +5 -5
  60. package/dist/contexts/index.mjs +4 -4
  61. package/dist/core/index.d.mts +31 -14
  62. package/dist/core/index.d.ts +31 -14
  63. package/dist/core/index.js +9 -5
  64. package/dist/core/index.js.map +1 -1
  65. package/dist/core/index.mjs +8 -4
  66. package/dist/features/analytics/index.css +30 -0
  67. package/dist/features/analytics/index.css.map +1 -0
  68. package/dist/features/analytics/index.d.mts +508 -0
  69. package/dist/features/analytics/index.d.ts +508 -0
  70. package/dist/features/analytics/index.js +1166 -0
  71. package/dist/features/analytics/index.js.map +1 -0
  72. package/dist/features/analytics/index.mjs +1166 -0
  73. package/dist/features/analytics/index.mjs.map +1 -0
  74. package/dist/features/analytics/server.d.mts +28 -0
  75. package/dist/features/analytics/server.d.ts +28 -0
  76. package/dist/features/analytics/server.js +41 -0
  77. package/dist/features/analytics/server.js.map +1 -0
  78. package/dist/features/analytics/server.mjs +41 -0
  79. package/dist/features/analytics/server.mjs.map +1 -0
  80. package/dist/features/help/index.d.mts +2 -2
  81. package/dist/features/help/index.d.ts +2 -2
  82. package/dist/features/help/index.js +43 -43
  83. package/dist/features/help/index.js.map +1 -1
  84. package/dist/features/help/index.mjs +9 -9
  85. package/dist/features/help/index.mjs.map +1 -1
  86. package/dist/features/tokenusage/index.d.mts +2 -2
  87. package/dist/features/tokenusage/index.d.ts +2 -2
  88. package/dist/features/tokenusage/index.js +141 -223
  89. package/dist/features/tokenusage/index.js.map +1 -1
  90. package/dist/features/tokenusage/index.mjs +28 -110
  91. package/dist/features/tokenusage/index.mjs.map +1 -1
  92. package/dist/index.d.mts +11 -9
  93. package/dist/index.d.ts +11 -9
  94. package/dist/index.js +10 -6
  95. package/dist/index.js.map +1 -1
  96. package/dist/index.mjs +9 -5
  97. package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
  98. package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
  99. package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
  100. package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
  101. package/dist/server/index.d.mts +5 -4
  102. package/dist/server/index.d.ts +5 -4
  103. package/dist/server/index.js +39 -279
  104. package/dist/server/index.js.map +1 -1
  105. package/dist/server/index.mjs +20 -260
  106. package/dist/server/index.mjs.map +1 -1
  107. package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
  108. package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
  109. package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
  110. package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
  111. package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
  112. package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
  113. package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
  114. package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
  115. package/package.json +11 -1
  116. package/src/client/index.ts +2 -0
  117. package/src/components/containers/PageContainer.tsx +1 -1
  118. package/src/components/containers/RoundPageContainer.tsx +60 -16
  119. package/src/components/containers/RoundPageContainerTitle.tsx +75 -35
  120. package/src/components/editors/BlockNoteEditor.tsx +13 -1
  121. package/src/components/forms/EntityMultiSelector.tsx +4 -4
  122. package/src/components/forms/EntitySelector.tsx +4 -4
  123. package/src/components/forms/FormBlockNote.tsx +21 -1
  124. package/src/components/forms/FormCheckbox.tsx +6 -2
  125. package/src/components/forms/__tests__/useEditorDialog.test.ts +47 -0
  126. package/src/components/forms/useEditorDialog.ts +22 -0
  127. package/src/components/navigations/Breadcrumb.tsx +45 -16
  128. package/src/components/navigations/Header.tsx +16 -3
  129. package/src/components/navigations/MobileNavigationBar.tsx +12 -0
  130. package/src/components/navigations/__tests__/MobileNavigationBar.spec.tsx +17 -0
  131. package/src/components/tables/ContentListTable.tsx +52 -32
  132. package/src/core/index.ts +4 -0
  133. package/src/core/registry/ModuleRegistry.ts +7 -0
  134. package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
  135. package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
  136. package/src/features/analytics/analytics.modules.ts +30 -0
  137. package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
  138. package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +120 -0
  139. package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
  140. package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
  141. package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +105 -0
  142. package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
  143. package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
  144. package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
  145. package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
  146. package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
  147. package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
  148. package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
  149. package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
  150. package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
  151. package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
  152. package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
  153. package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
  154. package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
  155. package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
  156. package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
  157. package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
  158. package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
  159. package/src/features/analytics/data/AnalyticsService.ts +23 -0
  160. package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
  161. package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
  162. package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
  163. package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
  164. package/src/features/analytics/data/analytics-breakdown.ts +65 -0
  165. package/src/features/analytics/data/analytics-event.interface.ts +7 -0
  166. package/src/features/analytics/data/analytics-event.ts +63 -0
  167. package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
  168. package/src/features/analytics/data/analytics-page-view.ts +57 -0
  169. package/src/features/analytics/data/analytics-session.interface.ts +23 -0
  170. package/src/features/analytics/data/analytics-session.ts +144 -0
  171. package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
  172. package/src/features/analytics/data/analytics-summary.ts +79 -0
  173. package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
  174. package/src/features/analytics/data/analytics-timeline.ts +67 -0
  175. package/src/features/analytics/data/analytics.types.ts +97 -0
  176. package/src/features/analytics/data/index.ts +15 -0
  177. package/src/features/analytics/i18n-keys.ts +76 -0
  178. package/src/features/analytics/index.ts +31 -0
  179. package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
  180. package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
  181. package/src/features/analytics/lib/utm.ts +29 -0
  182. package/src/features/analytics/lib/visitorCookie.ts +32 -0
  183. package/src/features/analytics/server-entry.ts +8 -0
  184. package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +1 -1
  185. package/src/features/assistant/components/parts/AssistantComposer.tsx +1 -1
  186. package/src/features/assistant-message/components/MessageItem.tsx +2 -2
  187. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
  188. package/src/features/auth/components/containers/AuthContainer.tsx +8 -1
  189. package/src/features/company/components/details/CompanyContent.tsx +3 -1
  190. package/src/features/company/components/details/CompanyDetails.tsx +9 -1
  191. package/src/features/company/components/details/TokenStatusIndicator.tsx +42 -13
  192. package/src/features/help/components/HelpHeader.tsx +7 -5
  193. package/src/features/tokenusage/components/TokenUsageAdminFilterBar.tsx +9 -6
  194. package/src/features/tokenusage/lib/__tests__/formatters.spec.ts +21 -0
  195. package/src/features/tokenusage/lib/formatters.ts +5 -1
  196. package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
  197. package/src/features/waitlist/data/WaitlistService.ts +2 -2
  198. package/src/features/waitlist/data/index.ts +1 -0
  199. package/src/features/waitlist/data/waitlist.fields.ts +8 -0
  200. package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
  201. package/src/shadcnui/ui/button.tsx +1 -1
  202. package/src/shadcnui/ui/checkbox.tsx +1 -1
  203. package/src/shadcnui/ui/command.tsx +4 -4
  204. package/src/shadcnui/ui/dialog.tsx +2 -0
  205. package/src/shadcnui/ui/input.tsx +1 -1
  206. package/src/shadcnui/ui/popover.tsx +9 -0
  207. package/src/shadcnui/ui/select.tsx +1 -1
  208. package/src/shadcnui/ui/sheet.tsx +8 -1
  209. package/src/shadcnui/ui/table.tsx +2 -2
  210. package/src/shadcnui/ui/textarea.tsx +1 -1
  211. package/dist/BlockNoteEditor-35D4R7HJ.js.map +0 -1
  212. package/dist/JsonApiRequest-G625QAH5.js +0 -26
  213. package/dist/chunk-2XCUAHQT.js.map +0 -1
  214. package/dist/chunk-4O7R2Z2O.mjs.map +0 -1
  215. package/dist/chunk-IBVKPLHT.mjs.map +0 -1
  216. package/dist/chunk-U7QVISQS.js.map +0 -1
  217. /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
  218. /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
  219. /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
  220. /package/dist/{chunk-ZJI5ZZW5.mjs.map → chunk-Q6J2ICKO.mjs.map} +0 -0
@@ -0,0 +1,140 @@
1
+ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AnalyticsAdminService } from "../../data/AnalyticsAdminService";
4
+ import { AnalyticsAdminProvider, useAnalyticsAdmin } from "../AnalyticsAdminContext";
5
+
6
+ vi.mock("../../data/AnalyticsAdminService", () => ({
7
+ AnalyticsAdminService: {
8
+ getSummary: vi.fn(async () => []),
9
+ getTimeline: vi.fn(async () => []),
10
+ getBreakdown: vi.fn(async () => []),
11
+ getSessions: vi.fn(async () => []),
12
+ next: vi.fn(async () => []),
13
+ },
14
+ }));
15
+
16
+ // The registry throws for modules nobody registered; the provider only needs
17
+ // the sessions module as the `type` of the load-more request.
18
+ vi.mock("../../../../core", async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import("../../../../core")>();
20
+ return {
21
+ ...actual,
22
+ Modules: { AnalyticsSession: { name: "analytics/administration/sessions" } },
23
+ };
24
+ });
25
+
26
+ function Probe() {
27
+ const ctx = useAnalyticsAdmin();
28
+ return (
29
+ <div>
30
+ <span data-testid="sessions">{ctx.sessions.map((s) => s.id).join(",")}</span>
31
+ <span data-testid="has-more">{String(ctx.hasMoreSessions)}</span>
32
+ <span data-testid="section">{ctx.filters.section}</span>
33
+ <button type="button" onClick={() => ctx.setFilters({ section: "app" })}>
34
+ app
35
+ </button>
36
+ <button type="button" onClick={() => void ctx.loadMoreSessions()}>
37
+ more
38
+ </button>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ describe("AnalyticsAdminContext", () => {
44
+ // The mocked service is module-level, so its call counts survive across tests.
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ });
48
+
49
+ it("throws when used outside its provider", () => {
50
+ expect(() => render(<Probe />)).toThrow();
51
+ });
52
+
53
+ it("requests every panel on mount: summary, timeline, one breakdown per dimension and the sessions", async () => {
54
+ render(
55
+ <AnalyticsAdminProvider>
56
+ <Probe />
57
+ </AnalyticsAdminProvider>,
58
+ );
59
+
60
+ await waitFor(() => expect(AnalyticsAdminService.getSessions).toHaveBeenCalledTimes(1));
61
+
62
+ expect(AnalyticsAdminService.getSummary).toHaveBeenCalledTimes(1);
63
+ expect(AnalyticsAdminService.getTimeline).toHaveBeenCalledWith(
64
+ expect.objectContaining({ section: "all", granularity: "day" }),
65
+ );
66
+
67
+ const breakdowns = vi
68
+ .mocked(AnalyticsAdminService.getBreakdown)
69
+ .mock.calls.map(([args]) => [args.dimension, args.limit]);
70
+ expect(breakdowns).toEqual(
71
+ expect.arrayContaining([
72
+ ["source", 20],
73
+ ["medium", 20],
74
+ ["campaign", 20],
75
+ ["referrer", 20],
76
+ ["route", 20],
77
+ ["landing", 20],
78
+ ]),
79
+ );
80
+ expect(breakdowns).toHaveLength(6);
81
+ });
82
+
83
+ it("defaults to the last 30 days", async () => {
84
+ render(
85
+ <AnalyticsAdminProvider>
86
+ <Probe />
87
+ </AnalyticsAdminProvider>,
88
+ );
89
+
90
+ await waitFor(() => expect(AnalyticsAdminService.getSummary).toHaveBeenCalled());
91
+
92
+ const { from, to } = vi.mocked(AnalyticsAdminService.getSummary).mock.calls[0][0];
93
+ const days = (Date.parse(to) - Date.parse(from)) / (24 * 60 * 60 * 1000);
94
+ expect(Math.round(days)).toBe(30);
95
+ });
96
+
97
+ it("refetches when a filter changes", async () => {
98
+ render(
99
+ <AnalyticsAdminProvider>
100
+ <Probe />
101
+ </AnalyticsAdminProvider>,
102
+ );
103
+ await waitFor(() => expect(AnalyticsAdminService.getSummary).toHaveBeenCalledTimes(1));
104
+
105
+ fireEvent.click(screen.getByText("app"));
106
+
107
+ await waitFor(() => expect(AnalyticsAdminService.getSummary).toHaveBeenCalledTimes(2));
108
+ expect(AnalyticsAdminService.getSummary).toHaveBeenLastCalledWith(expect.objectContaining({ section: "app" }));
109
+ expect(screen.getByTestId("section")).toHaveTextContent("app");
110
+ });
111
+
112
+ it("appends the next page of sessions on loadMoreSessions", async () => {
113
+ vi.mocked(AnalyticsAdminService.getSessions).mockImplementationOnce(async (params: any) => {
114
+ params.next.next = "https://api.example.com/analytics/administration/sessions?page[cursor]=2";
115
+ return [{ id: "s1" }] as any;
116
+ });
117
+ vi.mocked(AnalyticsAdminService.next).mockResolvedValueOnce([{ id: "s2" }] as any);
118
+
119
+ render(
120
+ <AnalyticsAdminProvider>
121
+ <Probe />
122
+ </AnalyticsAdminProvider>,
123
+ );
124
+
125
+ await waitFor(() => expect(screen.getByTestId("sessions")).toHaveTextContent("s1"));
126
+ expect(screen.getByTestId("has-more")).toHaveTextContent("true");
127
+
128
+ await act(async () => {
129
+ fireEvent.click(screen.getByText("more"));
130
+ });
131
+
132
+ await waitFor(() => expect(screen.getByTestId("sessions")).toHaveTextContent("s1,s2"));
133
+ expect(AnalyticsAdminService.next).toHaveBeenCalledWith(
134
+ expect.objectContaining({
135
+ endpoint: "https://api.example.com/analytics/administration/sessions?page[cursor]=2",
136
+ }),
137
+ );
138
+ expect(screen.getByTestId("has-more")).toHaveTextContent("false");
139
+ });
140
+ });
@@ -0,0 +1,91 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef } from "../../../core";
2
+ import { AnalyticsBreakdownInterface } from "./analytics-breakdown.interface";
3
+ import { AnalyticsPageViewInterface } from "./analytics-page-view.interface";
4
+ import { AnalyticsSessionInterface } from "./analytics-session.interface";
5
+ import { AnalyticsSummaryInterface } from "./analytics-summary.interface";
6
+ import { AnalyticsTimelineInterface } from "./analytics-timeline.interface";
7
+ import { AnalyticsDimension, AnalyticsFilters, AnalyticsGranularity } from "./analytics.types";
8
+
9
+ function withFilters(endpoint: EndpointCreator, filters: AnalyticsFilters): EndpointCreator {
10
+ endpoint.addAdditionalParam("from", filters.from);
11
+ endpoint.addAdditionalParam("to", filters.to);
12
+ endpoint.addAdditionalParam("section", filters.section);
13
+ return endpoint;
14
+ }
15
+
16
+ export class AnalyticsAdminService extends AbstractService {
17
+ /** Headline tiles: one row per window (current, previous). */
18
+ static async getSummary(filters: AnalyticsFilters): Promise<AnalyticsSummaryInterface[]> {
19
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.AnalyticsSummary }), filters);
20
+
21
+ return this.callApi<AnalyticsSummaryInterface[]>({
22
+ type: Modules.AnalyticsSummary,
23
+ method: HttpMethod.GET,
24
+ endpoint: endpoint.generate(),
25
+ });
26
+ }
27
+
28
+ static async getTimeline(
29
+ params: AnalyticsFilters & { granularity: AnalyticsGranularity },
30
+ ): Promise<AnalyticsTimelineInterface[]> {
31
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.AnalyticsTimeline }), params);
32
+ endpoint.addAdditionalParam("granularity", params.granularity);
33
+
34
+ return this.callApi<AnalyticsTimelineInterface[]>({
35
+ type: Modules.AnalyticsTimeline,
36
+ method: HttpMethod.GET,
37
+ endpoint: endpoint.generate(),
38
+ });
39
+ }
40
+
41
+ static async getBreakdown(
42
+ params: AnalyticsFilters & { dimension: AnalyticsDimension; limit?: number },
43
+ ): Promise<AnalyticsBreakdownInterface[]> {
44
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.AnalyticsBreakdown }), params);
45
+ endpoint.addAdditionalParam("dimension", params.dimension);
46
+ if (params.limit !== undefined) endpoint.addAdditionalParam("limit", String(params.limit));
47
+
48
+ return this.callApi<AnalyticsBreakdownInterface[]>({
49
+ type: Modules.AnalyticsBreakdown,
50
+ method: HttpMethod.GET,
51
+ endpoint: endpoint.generate(),
52
+ });
53
+ }
54
+
55
+ /** Newest sessions first, cursor-paginated through `next`. */
56
+ static async getSessions(
57
+ params: AnalyticsFilters & { visitorId?: string; userId?: string; next?: NextRef },
58
+ ): Promise<AnalyticsSessionInterface[]> {
59
+ const endpoint = withFilters(new EndpointCreator({ endpoint: Modules.AnalyticsSession }), params);
60
+ if (params.visitorId) endpoint.addAdditionalParam("visitorId", params.visitorId);
61
+ if (params.userId) endpoint.addAdditionalParam("userId", params.userId);
62
+
63
+ return this.callApi<AnalyticsSessionInterface[]>({
64
+ type: Modules.AnalyticsSession,
65
+ method: HttpMethod.GET,
66
+ endpoint: endpoint.generate(),
67
+ next: params.next,
68
+ });
69
+ }
70
+
71
+ static async getSession(params: { id: string }): Promise<AnalyticsSessionInterface> {
72
+ return this.callApi<AnalyticsSessionInterface>({
73
+ type: Modules.AnalyticsSession,
74
+ method: HttpMethod.GET,
75
+ endpoint: new EndpointCreator({ endpoint: Modules.AnalyticsSession, id: params.id }).generate(),
76
+ });
77
+ }
78
+
79
+ /** The journey: every page view of one session, oldest first. */
80
+ static async getSessionPageViews(params: { sessionId: string }): Promise<AnalyticsPageViewInterface[]> {
81
+ return this.callApi<AnalyticsPageViewInterface[]>({
82
+ type: Modules.AnalyticsPageView,
83
+ method: HttpMethod.GET,
84
+ endpoint: new EndpointCreator({
85
+ endpoint: Modules.AnalyticsSession,
86
+ id: params.sessionId,
87
+ childEndpoint: Modules.AnalyticsPageView,
88
+ }).generate(),
89
+ });
90
+ }
91
+ }
@@ -0,0 +1,23 @@
1
+ import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
+ import { AnalyticsEventInput } from "./analytics.types";
3
+
4
+ export class AnalyticsService extends AbstractService {
5
+ /**
6
+ * Posts one page view. Fire-and-forget: a lost page view must never surface
7
+ * an error to the visitor, so the global handler is suppressed and any
8
+ * rejection is logged at debug level and swallowed here.
9
+ */
10
+ static async track(input: AnalyticsEventInput): Promise<void> {
11
+ try {
12
+ await this.callApi({
13
+ type: Modules.AnalyticsEvent,
14
+ method: HttpMethod.POST,
15
+ endpoint: new EndpointCreator({ endpoint: Modules.AnalyticsEvent }).generate(),
16
+ input,
17
+ suppressGlobalError: true,
18
+ });
19
+ } catch (error) {
20
+ console.debug("Analytics event not sent", error);
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,117 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { AbstractService } from "../../../../core/abstracts/AbstractService";
3
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
4
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
5
+ import { ModuleFactory, ModuleWithPermissions } from "../../../../permissions/types";
6
+ import { analyticsModules } from "../../analytics.modules";
7
+ import { AnalyticsAdminService } from "../AnalyticsAdminService";
8
+
9
+ const moduleFactory: ModuleFactory = (params) =>
10
+ ({ pageUrl: params.pageUrl, name: params.name, model: params.model }) as ModuleWithPermissions;
11
+
12
+ // ModuleRegistry is shared across test files in a worker, hence the
13
+ // register-if-absent guard (same idiom as HandbookThreadService.spec.ts).
14
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
15
+ try {
16
+ ModuleRegistry.get(key as any);
17
+ } catch {
18
+ ModuleRegistry.register(key, module);
19
+ }
20
+ };
21
+
22
+ beforeAll(() => {
23
+ for (const [key, module] of Object.entries(analyticsModules(moduleFactory))) registerIfAbsent(key, module);
24
+ });
25
+
26
+ type CallApiParams = { type: ApiRequestDataTypeInterface; method: string; endpoint: string; next?: unknown };
27
+
28
+ // `callApi` is a protected static on AbstractService; spying on the base class
29
+ // intercepts every call without touching the HTTP layer.
30
+ const callApi = () => vi.spyOn(AbstractService as any, "callApi").mockResolvedValue([]);
31
+ const firstCall = (spy: ReturnType<typeof callApi>): CallApiParams => spy.mock.calls[0][0] as CallApiParams;
32
+
33
+ const filters = { from: "2026-09-01T00:00:00.000Z", to: "2026-10-01T00:00:00.000Z", section: "public" as const };
34
+
35
+ describe("AnalyticsAdminService", () => {
36
+ beforeEach(() => vi.restoreAllMocks());
37
+
38
+ it("getSummary sends from, to and section to the summary endpoint", async () => {
39
+ const spy = callApi();
40
+
41
+ await AnalyticsAdminService.getSummary(filters);
42
+
43
+ const call = firstCall(spy);
44
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsSummary" as any));
45
+ expect(call.method).toBe("GET");
46
+ expect(call.endpoint).toBe(
47
+ "analytics/administration/summary?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z&section=public",
48
+ );
49
+ });
50
+
51
+ it("getTimeline builds the endpoint from Modules.AnalyticsTimeline with from, to, section and granularity", async () => {
52
+ const spy = callApi();
53
+
54
+ await AnalyticsAdminService.getTimeline({ ...filters, granularity: "week" });
55
+
56
+ const call = firstCall(spy);
57
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsTimeline" as any));
58
+ expect(call.endpoint).toBe(
59
+ "analytics/administration/timeline?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z&section=public&granularity=week",
60
+ );
61
+ });
62
+
63
+ it("getBreakdown adds dimension and limit", async () => {
64
+ const spy = callApi();
65
+
66
+ await AnalyticsAdminService.getBreakdown({ ...filters, dimension: "source", limit: 50 });
67
+
68
+ const call = firstCall(spy);
69
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsBreakdown" as any));
70
+ expect(call.endpoint).toBe(
71
+ "analytics/administration/breakdown?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z&section=public&dimension=source&limit=50",
72
+ );
73
+ });
74
+
75
+ it("getBreakdown leaves limit out when it is not given", async () => {
76
+ const spy = callApi();
77
+
78
+ await AnalyticsAdminService.getBreakdown({ ...filters, dimension: "route" });
79
+
80
+ expect(firstCall(spy).endpoint).not.toContain("limit=");
81
+ });
82
+
83
+ it("getSessions forwards next and the visitor and user filters", async () => {
84
+ const spy = callApi();
85
+ const next = { next: "analytics/administration/sessions?page[cursor]=25" };
86
+
87
+ await AnalyticsAdminService.getSessions({ ...filters, visitorId: "v1", userId: "u1", next });
88
+
89
+ const call = firstCall(spy);
90
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsSession" as any));
91
+ expect(call.next).toBe(next);
92
+ expect(call.endpoint).toBe(
93
+ "analytics/administration/sessions?from=2026-09-01T00:00:00.000Z&to=2026-10-01T00:00:00.000Z&section=public&visitorId=v1&userId=u1",
94
+ );
95
+ });
96
+
97
+ it("getSession reads one session by id", async () => {
98
+ const spy = vi.spyOn(AbstractService as any, "callApi").mockResolvedValue({});
99
+
100
+ await AnalyticsAdminService.getSession({ id: "s1" });
101
+
102
+ const call = firstCall(spy);
103
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsSession" as any));
104
+ expect(call.endpoint).toBe("analytics/administration/sessions/s1");
105
+ });
106
+
107
+ it("getSessionPageViews uses the session endpoint with the pageviews child segment", async () => {
108
+ const spy = callApi();
109
+
110
+ await AnalyticsAdminService.getSessionPageViews({ sessionId: "s1" });
111
+
112
+ const call = firstCall(spy);
113
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsPageView" as any));
114
+ expect(call.method).toBe("GET");
115
+ expect(call.endpoint).toBe("analytics/administration/sessions/s1/pageviews");
116
+ });
117
+ });
@@ -0,0 +1,56 @@
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { AbstractService } from "../../../../core/abstracts/AbstractService";
3
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
4
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
5
+ import { ModuleFactory, ModuleWithPermissions } from "../../../../permissions/types";
6
+ import { analyticsModules } from "../../analytics.modules";
7
+ import { AnalyticsEventInput } from "../analytics.types";
8
+ import { AnalyticsService } from "../AnalyticsService";
9
+
10
+ const moduleFactory: ModuleFactory = (params) =>
11
+ ({ pageUrl: params.pageUrl, name: params.name, model: params.model }) as ModuleWithPermissions;
12
+
13
+ const registerIfAbsent = (key: string, module: ApiRequestDataTypeInterface) => {
14
+ try {
15
+ ModuleRegistry.get(key as any);
16
+ } catch {
17
+ ModuleRegistry.register(key, module);
18
+ }
19
+ };
20
+
21
+ beforeAll(() => {
22
+ for (const [key, module] of Object.entries(analyticsModules(moduleFactory))) registerIfAbsent(key, module);
23
+ });
24
+
25
+ type CallApiParams = {
26
+ type: ApiRequestDataTypeInterface;
27
+ method: string;
28
+ endpoint: string;
29
+ input?: unknown;
30
+ suppressGlobalError?: boolean;
31
+ };
32
+
33
+ const input: AnalyticsEventInput = { id: "e1", path: "/pricing", section: "public", utmSource: "linkedin" };
34
+
35
+ describe("AnalyticsService.track", () => {
36
+ beforeEach(() => vi.restoreAllMocks());
37
+
38
+ it("posts the input to Modules.AnalyticsEvent with the global error handler suppressed", async () => {
39
+ const spy = vi.spyOn(AbstractService as any, "callApi").mockResolvedValue({});
40
+
41
+ await AnalyticsService.track(input);
42
+
43
+ const call = spy.mock.calls[0][0] as CallApiParams;
44
+ expect(call.type).toBe(ModuleRegistry.get("AnalyticsEvent" as any));
45
+ expect(call.method).toBe("POST");
46
+ expect(call.endpoint).toBe("analytics/events");
47
+ expect(call.input).toBe(input);
48
+ expect(call.suppressGlobalError).toBe(true);
49
+ });
50
+
51
+ it("resolves even when callApi rejects — a lost page view never breaks the page", async () => {
52
+ vi.spyOn(AbstractService as any, "callApi").mockRejectedValue(new Error("network down"));
53
+
54
+ await expect(AnalyticsService.track(input)).resolves.toBeUndefined();
55
+ });
56
+ });