@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.
- package/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.ts +41 -24
- package/Models/AnalyticsModels/MetricBaselineHourly.ts +2 -0
- package/Models/AnalyticsModels/MetricItemAggMV1m.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByContainer.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.ts +5 -2
- package/Models/AnalyticsModels/MetricItemAggMV1mByService.ts +5 -2
- package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
- package/Models/DatabaseModels/GlobalConfig.ts +76 -0
- package/Server/API/EnterpriseLicenseAPI.ts +57 -0
- package/Server/API/GlobalConfigAPI.ts +62 -1
- package/Server/EnvironmentConfig.ts +22 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.ts +25 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AnalyticsDatabaseService.ts +61 -46
- package/Server/Services/DatabaseService.ts +66 -1
- package/Server/Services/WorkflowService.ts +100 -39
- package/Server/Types/Workflow/Components/Schedule.ts +57 -32
- package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +41 -4
- package/Server/Utils/Express.ts +13 -0
- package/Server/Utils/Telemetry/TelemetryFanInWriter.ts +799 -0
- package/Server/Utils/Telemetry/TelemetryWriterClient.ts +263 -0
- package/Server/Utils/Telemetry/TelemetryWriterServer.ts +240 -0
- package/Server/Utils/Telemetry/TelemetryWriterShedMetrics.ts +93 -0
- package/Tests/Models/AnalyticsModels/AnalyticsBaseModel.test.ts +122 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
- package/Tests/Server/Services/WorkflowService.test.ts +308 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +164 -3
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +99 -0
- package/Tests/Server/Utils/Attribution.test.ts +203 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +183 -0
- package/Tests/Server/Utils/Marketing/GoogleAds.test.ts +340 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria.test.ts +87 -0
- package/Tests/Server/Utils/Monitor/Criteria/LogMonitorCriteria.test.ts +165 -0
- package/Tests/Server/Utils/Monitor/Criteria/TraceMonitorCriteria.test.ts +86 -0
- package/Tests/Server/Utils/Telemetry/TelemetryFanInWriter.test.ts +1565 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterClient.test.ts +313 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterServer.test.ts +270 -0
- package/Tests/Server/Utils/Telemetry/TelemetryWriterShedMetrics.test.ts +127 -0
- package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -0
- package/Tests/Types/HashCode.test.ts +41 -0
- package/Tests/Types/Log/LogQueryParser.test.ts +186 -0
- package/Tests/Types/Monitor/MonitorCriteriaMetricVariables.test.ts +228 -0
- package/Tests/Types/Monitor/MonitorStepDefaultTelemetryConfig.test.ts +169 -0
- package/Tests/Types/Monitor/MonitorStepExceptionMonitor.test.ts +289 -0
- package/Tests/Types/Monitor/MonitorStepLogMonitor.test.ts +231 -0
- package/Tests/Types/Monitor/MonitorStepMetricViewConfigUtil.test.ts +245 -0
- package/Tests/Types/Monitor/MonitorStepNetworkDeviceMonitor.test.ts +155 -0
- package/Tests/Types/Monitor/MonitorStepTelemetrySerialization.test.ts +141 -0
- package/Tests/Types/Monitor/MonitorStepTraceMonitor.test.ts +137 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +311 -0
- package/Tests/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.test.ts +461 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +2852 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +758 -0
- package/Tests/UI/Utils/NotificationMethodUtil.test.ts +564 -0
- package/Tests/Utils/Array.test.ts +156 -0
- package/Tests/Utils/CronTab.test.ts +199 -0
- package/Tests/Utils/Dashboard/VariableInterpolation.test.ts +197 -0
- package/Tests/Utils/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/ValueFormatter.test.ts +305 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -0
- package/Types/AnalyticsDatabase/TableColumn.ts +23 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
- package/Types/Monitor/MonitorStep.ts +24 -4
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
- package/UI/Components/Workflow/ArgumentsForm.tsx +68 -30
- package/UI/Components/Workflow/CronScheduleField.tsx +411 -0
- package/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.ts +176 -0
- package/UI/Utils/NotificationMethodUtil.ts +310 -0
- package/Utils/CronTab.ts +823 -0
- package/Utils/ModelImportExport.ts +21 -6
- package/Utils/VersionUtil.ts +260 -0
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js +25 -20
- package/build/dist/Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js +2 -0
- package/build/dist/Models/AnalyticsModels/MetricBaselineHourly.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1m.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByContainer.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByHostV2.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByK8sCluster.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js +5 -2
- package/build/dist/Models/AnalyticsModels/MetricItemAggMV1mByService.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js +22 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicense.js.map +1 -1
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js +26 -0
- package/build/dist/Models/DatabaseModels/EnterpriseLicenseInstance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalConfig.js +81 -0
- package/build/dist/Models/DatabaseModels/GlobalConfig.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +47 -0
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/API/GlobalConfigAPI.js +52 -1
- package/build/dist/Server/API/GlobalConfigAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +17 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js +18 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784659816363-AddInstanceVersionAndLatestReleaseColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js +14 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784705487674-AddEnterpriseLicenseEvaluationColumns.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +53 -17
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +53 -1
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/WorkflowService.js +80 -30
- package/build/dist/Server/Services/WorkflowService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Schedule.js +44 -19
- package/build/dist/Server/Types/Workflow/Components/Schedule.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js +24 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/InsertDedupContext.js.map +1 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +29 -4
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Express.js +12 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js +496 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryFanInWriter.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js +165 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterClient.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js +136 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterServer.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js +76 -0
- package/build/dist/Server/Utils/Telemetry/TelemetryWriterShedMetrics.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumn.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +20 -4
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js +73 -0
- package/build/dist/Types/Monitor/MonitorStepMetricViewConfigUtil.js.map +1 -0
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js +261 -13
- package/build/dist/UI/Components/EditionLabel/EditionLabel.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +30 -6
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/CronScheduleField.js +209 -0
- package/build/dist/UI/Components/Workflow/CronScheduleField.js.map +1 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js +129 -0
- package/build/dist/UI/Utils/Breadcrumb/BreadcrumbTrailResolver.js.map +1 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js +189 -0
- package/build/dist/UI/Utils/NotificationMethodUtil.js.map +1 -0
- package/build/dist/Utils/CronTab.js +609 -0
- package/build/dist/Utils/CronTab.js.map +1 -0
- package/build/dist/Utils/ModelImportExport.js +20 -6
- package/build/dist/Utils/ModelImportExport.js.map +1 -1
- package/build/dist/Utils/VersionUtil.js +193 -0
- package/build/dist/Utils/VersionUtil.js.map +1 -0
- 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
|
+
});
|