@oneuptime/common 11.5.13 → 11.6.1

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 (156) hide show
  1. package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
  2. package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
  3. package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
  4. package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
  5. package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
  6. package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
  7. package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
  8. package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
  9. package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
  10. package/Models/DatabaseModels/GlobalConfig.ts +76 -0
  11. package/Server/API/EnterpriseLicenseAPI.ts +57 -0
  12. package/Server/API/GlobalConfigAPI.ts +62 -1
  13. package/Server/EnvironmentConfig.ts +22 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -46
  18. package/Server/Services/DatabaseService.ts +66 -1
  19. package/Server/Services/WorkflowService.ts +100 -39
  20. package/Server/Types/Workflow/Components/Schedule.ts +57 -32
  21. package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
  22. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
  23. package/Server/Utils/Express.ts +13 -0
  24. package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
  25. package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
  26. package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
  27. package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
  28. package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
  29. package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
  30. package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
  31. package/Tests/Server/Services/WorkflowService.test.ts +308 -0
  32. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
  33. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
  34. package/Tests/Server/Utils/Attribution.test.ts +203 -0
  35. package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
  36. package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
  37. package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
  38. package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
  39. package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
  40. package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
  41. package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
  42. package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
  43. package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
  44. package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
  45. package/Tests/Types/HashCode.test.ts +41 -0
  46. package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
  47. package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
  48. package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
  49. package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
  50. package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
  51. package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
  52. package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
  53. package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
  54. package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
  55. package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
  56. package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
  57. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
  58. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
  59. package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
  60. package/Tests/Utils/Array.test.ts +156 -0
  61. package/Tests/Utils/CronTab.test.ts +199 -0
  62. package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
  63. package/Tests/Utils/ModelImportExport.test.ts +101 -0
  64. package/Tests/Utils/Number.test.ts +179 -0
  65. package/Tests/Utils/ValueFormatter.test.ts +305 -0
  66. package/Tests/Utils/VersionUtil.test.ts +348 -0
  67. package/Tests/jest.setup.ts +1 -0
  68. package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
  69. package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
  70. package/Types/Monitor/MonitorStep.ts +24 -4
  71. package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
  72. package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
  73. package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
  74. package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
  75. package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
  76. package/UI/Utils/NotificationMethodUtil.ts +310 -0
  77. package/Utils/CronTab.ts +823 -0
  78. package/Utils/ModelImportExport.ts +21 -6
  79. package/Utils/VersionUtil.ts +260 -0
  80. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
  81. package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
  82. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
  83. package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
  84. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
  85. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
  86. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
  87. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
  88. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
  89. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
  90. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
  91. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
  92. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
  93. package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
  94. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
  95. package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
  96. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
  97. package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
  98. package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
  99. package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
  100. package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
  101. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  102. package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
  103. package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
  104. package/build/dist/Server/EnvironmentConfig.js +17 -0
  105. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  106. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
  107. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
  108. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
  109. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
  110. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  111. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  112. package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
  113. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  114. package/build/dist/Server/Services/DatabaseService.js +53 -1
  115. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  116. package/build/dist/Server/Services/WorkflowService.js +80 -30
  117. package/build/dist/Server/Services/WorkflowService.js.map +1 -1
  118. package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
  119. package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
  120. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
  121. package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
  122. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
  123. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  124. package/build/dist/Server/Utils/Express.js +12 -0
  125. package/build/dist/Server/Utils/Express.js.map +1 -1
  126. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
  127. package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
  128. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
  129. package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
  130. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
  131. package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
  132. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
  133. package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
  134. package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
  135. package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
  136. package/build/dist/Types/Monitor/MonitorStep.js +20 -4
  137. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  138. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
  139. package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
  140. package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
  141. package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
  142. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
  143. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  144. package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
  145. package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
  146. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
  147. package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
  148. package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
  149. package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
  150. package/build/dist/Utils/CronTab.js +609 -0
  151. package/build/dist/Utils/CronTab.js.map +1 -0
  152. package/build/dist/Utils/ModelImportExport.js +20 -6
  153. package/build/dist/Utils/ModelImportExport.js.map +1 -1
  154. package/build/dist/Utils/VersionUtil.js +193 -0
  155. package/build/dist/Utils/VersionUtil.js.map +1 -0
  156. package/package.json +1 -1
@@ -0,0 +1,203 @@
1
+ import Attribution from "../../../Server/Utils/Attribution";
2
+ import { JSONObject } from "../../../Types/JSON";
3
+ import { describe, expect, test } from "@jest/globals";
4
+
5
+ describe("Attribution", () => {
6
+ describe("sanitizeClickIds", () => {
7
+ test("keeps every supported ad-platform click identifier", () => {
8
+ const clickIds: JSONObject = {
9
+ gclid: "google-click",
10
+ wbraid: "google-web-to-app",
11
+ gbraid: "google-app-to-web",
12
+ fbclid: "meta-click",
13
+ msclkid: "microsoft-click",
14
+ li_fat_id: "linkedin-click",
15
+ twclid: "x-click",
16
+ rdt_cid: "reddit-click",
17
+ };
18
+
19
+ expect(Attribution.sanitizeClickIds(clickIds)).toEqual(clickIds);
20
+ });
21
+
22
+ test("drops keys that are not explicitly allowlisted", () => {
23
+ expect(
24
+ Attribution.sanitizeClickIds({
25
+ gclid: "valid",
26
+ attackerControlledKey: "must-not-persist",
27
+ __proto__: "must-not-persist",
28
+ }),
29
+ ).toEqual({ gclid: "valid" });
30
+ });
31
+
32
+ test.each([null, undefined, "gclid=x", 42, true, ["gclid"]])(
33
+ "rejects a non-object value: %p",
34
+ (value: unknown) => {
35
+ expect(Attribution.sanitizeClickIds(value as never)).toBeUndefined();
36
+ },
37
+ );
38
+
39
+ test("drops empty, boolean and object click-id values", () => {
40
+ expect(
41
+ Attribution.sanitizeClickIds({
42
+ gclid: "",
43
+ fbclid: false,
44
+ msclkid: { nested: "value" },
45
+ }),
46
+ ).toBeUndefined();
47
+ });
48
+
49
+ test("normalizes numeric identifiers to strings", () => {
50
+ expect(
51
+ Attribution.sanitizeClickIds({
52
+ gclid: 12345,
53
+ msclkid: 67890,
54
+ }),
55
+ ).toEqual({
56
+ gclid: "12345",
57
+ msclkid: "67890",
58
+ });
59
+ });
60
+
61
+ test("caps each identifier at 500 characters", () => {
62
+ const result: JSONObject | undefined = Attribution.sanitizeClickIds({
63
+ gclid: "g".repeat(600),
64
+ fbclid: "f".repeat(501),
65
+ });
66
+
67
+ expect(result?.["gclid"]).toBe("g".repeat(500));
68
+ expect(result?.["fbclid"]).toBe("f".repeat(500));
69
+ });
70
+
71
+ test("returns a fresh object instead of retaining the untrusted input", () => {
72
+ const input: JSONObject = { gclid: "original" };
73
+ const result: JSONObject | undefined =
74
+ Attribution.sanitizeClickIds(input);
75
+
76
+ input["gclid"] = "mutated";
77
+
78
+ expect(result).toEqual({ gclid: "original" });
79
+ expect(result).not.toBe(input);
80
+ });
81
+ });
82
+
83
+ describe("sanitizeFirstTouchAttribution", () => {
84
+ test("keeps the complete supported first-touch payload", () => {
85
+ const firstTouch: JSONObject = {
86
+ utmSource: "google",
87
+ utmMedium: "cpc",
88
+ utmCampaign: "pagerduty-alternative",
89
+ utmTerm: "pagerduty alternative",
90
+ utmContent: "comparison-ad-a",
91
+ landingUrl: "https://oneuptime.com/compare/pagerduty?gclid=abc",
92
+ referrer: "https://google.com/",
93
+ timestamp: "2026-07-22T10:00:00.000Z",
94
+ clickIds: {
95
+ gclid: "abc",
96
+ msclkid: "def",
97
+ },
98
+ };
99
+
100
+ expect(Attribution.sanitizeFirstTouchAttribution(firstTouch)).toEqual(
101
+ firstTouch,
102
+ );
103
+ });
104
+
105
+ test("drops unknown top-level and nested click-id keys", () => {
106
+ expect(
107
+ Attribution.sanitizeFirstTouchAttribution({
108
+ utmSource: "google",
109
+ arbitrary: "do-not-store",
110
+ clickIds: {
111
+ gclid: "valid",
112
+ arbitraryClickId: "do-not-store",
113
+ },
114
+ }),
115
+ ).toEqual({
116
+ utmSource: "google",
117
+ clickIds: { gclid: "valid" },
118
+ });
119
+ });
120
+
121
+ test("keeps valid first-touch fields when clickIds is malformed", () => {
122
+ expect(
123
+ Attribution.sanitizeFirstTouchAttribution({
124
+ utmSource: "newsletter",
125
+ clickIds: ["not", "an", "object"],
126
+ }),
127
+ ).toEqual({ utmSource: "newsletter" });
128
+ });
129
+
130
+ test("keeps valid click IDs when no UTM values exist", () => {
131
+ expect(
132
+ Attribution.sanitizeFirstTouchAttribution({
133
+ clickIds: { gclid: "auto-tagged-click" },
134
+ }),
135
+ ).toEqual({
136
+ clickIds: { gclid: "auto-tagged-click" },
137
+ });
138
+ });
139
+
140
+ test("caps all first-touch strings and nested click IDs", () => {
141
+ const result: JSONObject | undefined =
142
+ Attribution.sanitizeFirstTouchAttribution({
143
+ utmCampaign: "c".repeat(700),
144
+ landingUrl: "l".repeat(700),
145
+ clickIds: { gclid: "g".repeat(700) },
146
+ });
147
+
148
+ expect(result?.["utmCampaign"]).toBe("c".repeat(500));
149
+ expect(result?.["landingUrl"]).toBe("l".repeat(500));
150
+ expect((result?.["clickIds"] as JSONObject)?.["gclid"]).toBe(
151
+ "g".repeat(500),
152
+ );
153
+ });
154
+
155
+ test("normalizes numeric scalar values", () => {
156
+ expect(
157
+ Attribution.sanitizeFirstTouchAttribution({
158
+ utmCampaign: 2026,
159
+ timestamp: 123456,
160
+ }),
161
+ ).toEqual({
162
+ utmCampaign: "2026",
163
+ timestamp: "123456",
164
+ });
165
+ });
166
+
167
+ test.each([null, undefined, "utm_source=google", 42, true, []])(
168
+ "rejects a non-object first-touch value: %p",
169
+ (value: unknown) => {
170
+ expect(
171
+ Attribution.sanitizeFirstTouchAttribution(value as never),
172
+ ).toBeUndefined();
173
+ },
174
+ );
175
+
176
+ test("returns undefined when every supplied field is invalid", () => {
177
+ expect(
178
+ Attribution.sanitizeFirstTouchAttribution({
179
+ unsupported: "value",
180
+ clickIds: { unsupported: "value" },
181
+ }),
182
+ ).toBeUndefined();
183
+ });
184
+
185
+ test("does not retain mutable nested input objects", () => {
186
+ const clickIds: JSONObject = { gclid: "original" };
187
+ const input: JSONObject = {
188
+ utmSource: "google",
189
+ clickIds,
190
+ };
191
+ const result: JSONObject | undefined =
192
+ Attribution.sanitizeFirstTouchAttribution(input);
193
+
194
+ clickIds["gclid"] = "mutated";
195
+ input["utmSource"] = "mutated";
196
+
197
+ expect(result).toEqual({
198
+ utmSource: "google",
199
+ clickIds: { gclid: "original" },
200
+ });
201
+ });
202
+ });
203
+ });
@@ -0,0 +1,183 @@
1
+ import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
2
+ import ConversionUploadProvider, {
3
+ ConversionSkip,
4
+ ConversionUploadBatchResult,
5
+ } from "../../../../Server/Utils/Marketing/ConversionUploadProvider";
6
+ import { MarketingConversionType } from "../../../../Types/Marketing/MarketingConversion";
7
+ import { AxiosError } from "axios";
8
+ import { afterEach, describe, expect, jest, test } from "@jest/globals";
9
+
10
+ class TestProvider extends ConversionUploadProvider {
11
+ public override readonly key: string = "test";
12
+ public override readonly displayName: string = "Test Provider";
13
+
14
+ public override isConfigured(): boolean {
15
+ return true;
16
+ }
17
+
18
+ public override getSkipReason(
19
+ _conversion: MarketingConversion,
20
+ ): ConversionSkip | null {
21
+ return null;
22
+ }
23
+
24
+ public override async upload(
25
+ _conversions: Array<MarketingConversion>,
26
+ ): Promise<ConversionUploadBatchResult> {
27
+ return { permanentFailures: new Map<number, string>() };
28
+ }
29
+
30
+ public readClickId(
31
+ conversion: MarketingConversion,
32
+ key: string,
33
+ ): string | undefined {
34
+ return this.getClickId(conversion, key);
35
+ }
36
+
37
+ public readValueInUSD(conversion: MarketingConversion): number | undefined {
38
+ return this.getValueInUSD(conversion);
39
+ }
40
+
41
+ public readIsSignUp(conversion: MarketingConversion): boolean {
42
+ return this.isSignUp(conversion);
43
+ }
44
+
45
+ public readIsOlderThanDays(
46
+ conversion: MarketingConversion,
47
+ days: number,
48
+ ): boolean {
49
+ return this.isOlderThanDays(conversion, days);
50
+ }
51
+ }
52
+
53
+ const makeConversion: (
54
+ data?: Partial<MarketingConversion>,
55
+ ) => MarketingConversion = (
56
+ data: Partial<MarketingConversion> = {},
57
+ ): MarketingConversion => {
58
+ return Object.assign(new MarketingConversion(), data);
59
+ };
60
+
61
+ describe("ConversionUploadProvider", () => {
62
+ const provider: TestProvider = new TestProvider();
63
+
64
+ afterEach(() => {
65
+ jest.restoreAllMocks();
66
+ });
67
+
68
+ test("extracts a non-empty string click ID", () => {
69
+ const conversion: MarketingConversion = makeConversion({
70
+ clickIds: { gclid: "google-click" },
71
+ });
72
+
73
+ expect(provider.readClickId(conversion, "gclid")).toBe("google-click");
74
+ });
75
+
76
+ test.each([undefined, null, "", 0, 123, false, { nested: true }])(
77
+ "rejects an invalid click ID value: %p",
78
+ (value: unknown) => {
79
+ const conversion: MarketingConversion = makeConversion({
80
+ clickIds: { gclid: value as never },
81
+ });
82
+
83
+ expect(provider.readClickId(conversion, "gclid")).toBeUndefined();
84
+ },
85
+ );
86
+
87
+ test("returns undefined when click IDs are absent", () => {
88
+ const conversion: MarketingConversion = makeConversion();
89
+ delete conversion.clickIds;
90
+
91
+ expect(provider.readClickId(conversion, "gclid")).toBeUndefined();
92
+ });
93
+
94
+ test("converts cents to an exact USD decimal", () => {
95
+ expect(
96
+ provider.readValueInUSD(
97
+ makeConversion({ conversionValueInUSDCents: 12345 }),
98
+ ),
99
+ ).toBe(123.45);
100
+ });
101
+
102
+ test.each([undefined, null, 0, -1, -100])(
103
+ "omits a non-positive or missing conversion value: %p",
104
+ (value: number | null | undefined) => {
105
+ const conversion: MarketingConversion = makeConversion();
106
+ conversion.conversionValueInUSDCents = value as never;
107
+
108
+ expect(provider.readValueInUSD(conversion)).toBeUndefined();
109
+ },
110
+ );
111
+
112
+ test("classifies only the SignUp conversion type as a signup", () => {
113
+ expect(
114
+ provider.readIsSignUp(
115
+ makeConversion({ conversionType: MarketingConversionType.SignUp }),
116
+ ),
117
+ ).toBe(true);
118
+ expect(
119
+ provider.readIsSignUp(
120
+ makeConversion({
121
+ conversionType: MarketingConversionType.PaidSubscription,
122
+ }),
123
+ ),
124
+ ).toBe(false);
125
+ });
126
+
127
+ test("checks conversion age against the provider window", () => {
128
+ jest
129
+ .spyOn(Date, "now")
130
+ .mockReturnValue(new Date("2026-07-22T12:00:00.000Z").getTime());
131
+
132
+ expect(
133
+ provider.readIsOlderThanDays(
134
+ makeConversion({
135
+ conversionAt: new Date("2026-04-22T11:59:59.999Z"),
136
+ }),
137
+ 90,
138
+ ),
139
+ ).toBe(true);
140
+ expect(
141
+ provider.readIsOlderThanDays(
142
+ makeConversion({
143
+ conversionAt: new Date("2026-07-21T12:00:00.000Z"),
144
+ }),
145
+ 90,
146
+ ),
147
+ ).toBe(false);
148
+ });
149
+
150
+ test("uses the current time when conversionAt is missing", () => {
151
+ jest
152
+ .spyOn(Date, "now")
153
+ .mockReturnValue(new Date("2026-07-22T12:00:00.000Z").getTime());
154
+
155
+ expect(provider.readIsOlderThanDays(makeConversion(), 90)).toBe(false);
156
+ });
157
+
158
+ test("formats ordinary errors without leaking an object dump", () => {
159
+ expect(
160
+ ConversionUploadProvider.getErrorMessage(new Error("network down")),
161
+ ).toBe("network down");
162
+ });
163
+
164
+ test("formats Axios errors with status and bounded response data", () => {
165
+ const error: AxiosError = new AxiosError("rate limited");
166
+ Object.assign(error, {
167
+ response: {
168
+ status: 429,
169
+ data: { error: "retry later" },
170
+ },
171
+ });
172
+
173
+ expect(ConversionUploadProvider.getErrorMessage(error)).toBe(
174
+ 'HTTP 429: {"error":"retry later"}',
175
+ );
176
+ });
177
+
178
+ test("falls back to Unknown error for non-error values", () => {
179
+ expect(ConversionUploadProvider.getErrorMessage(null)).toBe(
180
+ "Unknown error",
181
+ );
182
+ });
183
+ });