@oneuptime/common 11.5.12 → 11.6.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.
- package/Models/DatabaseModels/AIInsight.ts +30 -0
- package/Models/DatabaseModels/EnterpriseLicense.ts +21 -0
- package/Models/DatabaseModels/EnterpriseLicenseInstance.ts +25 -0
- package/Models/DatabaseModels/GlobalConfig.ts +76 -0
- package/Models/DatabaseModels/Project.ts +30 -0
- package/Models/DatabaseModels/TelemetryException.ts +128 -0
- package/Server/API/AIAgentDataAPI.ts +129 -5
- package/Server/API/AIInvestigationAPI.ts +87 -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/1784640000000-AddExceptionTriageAndPrivacyColumns.ts +56 -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 +6 -0
- package/Server/Services/AIRunService.ts +8 -0
- package/Server/Services/AnalyticsDatabaseService.ts +48 -45
- package/Server/Services/DatabaseService.ts +66 -1
- package/Server/Services/TelemetryExceptionService.ts +320 -24
- package/Server/Services/WorkflowService.ts +100 -39
- package/Server/Types/Workflow/Components/Schedule.ts +57 -32
- package/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.ts +15 -4
- package/Server/Utils/AI/SRE/Insights/InsightScanner.ts +222 -24
- package/Server/Utils/AI/SRE/Insights/InsightTriageRunner.ts +225 -9
- package/Server/Utils/AI/SRE/SubjectCodeFixRun.ts +39 -0
- package/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.ts +114 -0
- package/Server/Utils/AnalyticsDatabase/InsertDedupContext.ts +59 -0
- package/Server/Utils/Express.ts +13 -0
- package/Server/Utils/Telemetry/ExceptionSanitizer.ts +222 -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/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +232 -0
- package/Tests/Server/Services/TelemetryExceptionCodeFixRun.test.ts +205 -1
- package/Tests/Server/Services/UpdateOneByIdKeepsRowId.test.ts +107 -0
- package/Tests/Server/Services/WorkflowService.test.ts +308 -0
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +150 -141
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +277 -1
- package/Tests/Server/Utils/Attribution.test.ts +203 -0
- package/Tests/Server/Utils/ExceptionSanitizer.test.ts +70 -0
- package/Tests/Server/Utils/InsightTriageClassification.test.ts +57 -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/Server/Utils/TelemetryImprovementTaskTrigger.test.ts +141 -0
- package/Tests/Types/AI/CodeFixTaskType.test.ts +3 -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/Components/ModelTableSelectFromColumns.test.ts +259 -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/ModelImportExport.test.ts +101 -0
- package/Tests/Utils/Number.test.ts +179 -0
- package/Tests/Utils/VersionUtil.test.ts +348 -0
- package/Tests/jest.setup.ts +1 -0
- package/Types/AI/CodeFixTaskContext.ts +6 -0
- package/Types/AI/CodeFixTaskType.ts +38 -1
- package/Types/AI/ExceptionAIClassification.ts +27 -0
- package/Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary.ts +6 -0
- package/Types/Log/LogScrubPatternType.ts +7 -0
- package/Types/Monitor/MonitorStep.ts +24 -4
- package/Types/Monitor/MonitorStepMetricViewConfigUtil.ts +105 -0
- package/Types/Trace/TraceScrubPatternType.ts +7 -0
- package/UI/Components/EditionLabel/EditionLabel.tsx +445 -12
- package/UI/Components/ModelTable/BaseModelTable.tsx +15 -46
- package/UI/Components/ModelTable/SelectFromColumns.ts +129 -0
- 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/DatabaseModels/AIInsight.js +31 -0
- package/build/dist/Models/DatabaseModels/AIInsight.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/Models/DatabaseModels/Project.js +31 -0
- package/build/dist/Models/DatabaseModels/Project.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TelemetryException.js +134 -0
- package/build/dist/Models/DatabaseModels/TelemetryException.js.map +1 -1
- package/build/dist/Server/API/AIAgentDataAPI.js +97 -8
- package/build/dist/Server/API/AIAgentDataAPI.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +55 -0
- package/build/dist/Server/API/AIInvestigationAPI.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/1784640000000-AddExceptionTriageAndPrivacyColumns.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1784640000000-AddExceptionTriageAndPrivacyColumns.js.map +1 -0
- 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 +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AIRunService.js +9 -2
- package/build/dist/Server/Services/AIRunService.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +44 -16
- 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/TelemetryExceptionService.js +263 -22
- package/build/dist/Server/Services/TelemetryExceptionService.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/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/ExceptionSpikeDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js +12 -4
- package/build/dist/Server/Utils/AI/SRE/Insights/Detectors/NewExceptionDetector.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js +193 -22
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightScanner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js +178 -9
- package/build/dist/Server/Utils/AI/SRE/Insights/InsightTriageRunner.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js +37 -0
- package/build/dist/Server/Utils/AI/SRE/SubjectCodeFixRun.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js +98 -0
- package/build/dist/Server/Utils/AI/SRE/TelemetryImprovementTaskTrigger.js.map +1 -0
- 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/Express.js +12 -0
- package/build/dist/Server/Utils/Express.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js +149 -0
- package/build/dist/Server/Utils/Telemetry/ExceptionSanitizer.js.map +1 -0
- 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/AI/CodeFixTaskType.js +38 -1
- package/build/dist/Types/AI/CodeFixTaskType.js.map +1 -1
- package/build/dist/Types/AI/ExceptionAIClassification.js +28 -0
- package/build/dist/Types/AI/ExceptionAIClassification.js.map +1 -0
- package/build/dist/Types/Log/LogScrubPatternType.js +7 -0
- package/build/dist/Types/Log/LogScrubPatternType.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/Types/Trace/TraceScrubPatternType.js +7 -0
- package/build/dist/Types/Trace/TraceScrubPatternType.js.map +1 -1
- 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/ModelTable/BaseModelTable.js +12 -37
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js +82 -0
- package/build/dist/UI/Components/ModelTable/SelectFromColumns.js.map +1 -0
- 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,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
|
+
});
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import MarketingConversion from "../../../../Models/DatabaseModels/MarketingConversion";
|
|
2
|
+
import logger from "../../../../Server/Utils/Logger";
|
|
3
|
+
import GoogleAdsProvider from "../../../../Server/Utils/Marketing/Providers/GoogleAds";
|
|
4
|
+
import { ConversionUploadBatchResult } from "../../../../Server/Utils/Marketing/ConversionUploadProvider";
|
|
5
|
+
import { JSONObject } from "../../../../Types/JSON";
|
|
6
|
+
import { MarketingConversionType } from "../../../../Types/Marketing/MarketingConversion";
|
|
7
|
+
import axios, { AxiosResponse } from "axios";
|
|
8
|
+
import {
|
|
9
|
+
afterEach,
|
|
10
|
+
beforeEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
jest,
|
|
14
|
+
test,
|
|
15
|
+
} from "@jest/globals";
|
|
16
|
+
import { SpyInstance } from "jest-mock";
|
|
17
|
+
|
|
18
|
+
jest.mock("../../../../Server/EnvironmentConfig", () => {
|
|
19
|
+
return {
|
|
20
|
+
GoogleAdsApiVersion: "v23",
|
|
21
|
+
GoogleAdsCustomerId: "1234567890",
|
|
22
|
+
GoogleAdsDeveloperToken: "developer-token",
|
|
23
|
+
GoogleAdsLoginCustomerId: "0987654321",
|
|
24
|
+
GoogleAdsOAuthClientId: "oauth-client-id",
|
|
25
|
+
GoogleAdsOAuthClientSecret: "oauth-client-secret",
|
|
26
|
+
GoogleAdsOAuthRefreshToken: "oauth-refresh-token",
|
|
27
|
+
GoogleAdsPaidSubscriptionConversionActionId: "222222",
|
|
28
|
+
GoogleAdsSignUpConversionActionId: "111111",
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
jest.mock("../../../../Server/Utils/Logger", () => {
|
|
33
|
+
return {
|
|
34
|
+
__esModule: true,
|
|
35
|
+
default: {
|
|
36
|
+
error: jest.fn(),
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const makeConversion: (
|
|
42
|
+
data?: Partial<MarketingConversion>,
|
|
43
|
+
) => MarketingConversion = (
|
|
44
|
+
data: Partial<MarketingConversion> = {},
|
|
45
|
+
): MarketingConversion => {
|
|
46
|
+
return Object.assign(new MarketingConversion(), {
|
|
47
|
+
conversionType: MarketingConversionType.SignUp,
|
|
48
|
+
conversionAt: new Date("2026-07-22T10:11:12.345Z"),
|
|
49
|
+
clickIds: { gclid: "google-click" },
|
|
50
|
+
...data,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const response: (data: JSONObject) => AxiosResponse<JSONObject> = (
|
|
55
|
+
data: JSONObject,
|
|
56
|
+
): AxiosResponse<JSONObject> => {
|
|
57
|
+
return { data } as AxiosResponse<JSONObject>;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe("GoogleAdsProvider", () => {
|
|
61
|
+
let provider: GoogleAdsProvider;
|
|
62
|
+
let postSpy: SpyInstance<any>;
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
provider = new GoogleAdsProvider();
|
|
66
|
+
postSpy = jest.spyOn(axios, "post") as SpyInstance<any>;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
jest.restoreAllMocks();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("reports configured only when the OAuth and account settings exist", () => {
|
|
74
|
+
expect(provider.isConfigured()).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("permanently skips conversions without a Google click identifier", () => {
|
|
78
|
+
expect(
|
|
79
|
+
provider.getSkipReason(makeConversion({ clickIds: { fbclid: "meta" } })),
|
|
80
|
+
).toEqual({
|
|
81
|
+
reason: "No Google click id (gclid/wbraid/gbraid)",
|
|
82
|
+
isPermanent: true,
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.each([
|
|
87
|
+
["gclid", "standard-click"],
|
|
88
|
+
["wbraid", "web-to-app-click"],
|
|
89
|
+
["gbraid", "app-to-web-click"],
|
|
90
|
+
])("accepts the supported %s identifier", (key: string, value: string) => {
|
|
91
|
+
expect(
|
|
92
|
+
provider.getSkipReason(makeConversion({ clickIds: { [key]: value } })),
|
|
93
|
+
).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("prefers gclid when multiple Google identifiers are present", async () => {
|
|
97
|
+
postSpy
|
|
98
|
+
.mockResolvedValueOnce(
|
|
99
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
100
|
+
)
|
|
101
|
+
.mockResolvedValueOnce(response({}) as never);
|
|
102
|
+
|
|
103
|
+
await provider.upload([
|
|
104
|
+
makeConversion({
|
|
105
|
+
clickIds: {
|
|
106
|
+
gclid: "preferred-gclid",
|
|
107
|
+
wbraid: "secondary-wbraid",
|
|
108
|
+
gbraid: "secondary-gbraid",
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const uploadBody: JSONObject = postSpy.mock.calls[1]?.[1] as JSONObject;
|
|
114
|
+
const uploadedConversion: JSONObject = (
|
|
115
|
+
uploadBody["conversions"] as Array<JSONObject>
|
|
116
|
+
)[0]!;
|
|
117
|
+
|
|
118
|
+
expect(uploadedConversion).toMatchObject({ gclid: "preferred-gclid" });
|
|
119
|
+
expect(uploadedConversion["wbraid"]).toBeUndefined();
|
|
120
|
+
expect(uploadedConversion["gbraid"]).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("permanently skips conversions outside Google's 90-day window", () => {
|
|
124
|
+
jest
|
|
125
|
+
.spyOn(Date, "now")
|
|
126
|
+
.mockReturnValue(new Date("2026-07-22T12:00:00.000Z").getTime());
|
|
127
|
+
|
|
128
|
+
expect(
|
|
129
|
+
provider.getSkipReason(
|
|
130
|
+
makeConversion({
|
|
131
|
+
conversionAt: new Date("2026-04-01T00:00:00.000Z"),
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
).toEqual({
|
|
135
|
+
reason: "Conversion older than Google's 90-day click window",
|
|
136
|
+
isPermanent: true,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("uploads signup and paid conversions with the correct actions and value", async () => {
|
|
141
|
+
postSpy
|
|
142
|
+
.mockResolvedValueOnce(
|
|
143
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
144
|
+
)
|
|
145
|
+
.mockResolvedValueOnce(response({}) as never);
|
|
146
|
+
|
|
147
|
+
const result: ConversionUploadBatchResult = await provider.upload([
|
|
148
|
+
makeConversion({
|
|
149
|
+
conversionType: MarketingConversionType.SignUp,
|
|
150
|
+
clickIds: { wbraid: "signup-wbraid" },
|
|
151
|
+
}),
|
|
152
|
+
makeConversion({
|
|
153
|
+
conversionType: MarketingConversionType.PaidSubscription,
|
|
154
|
+
clickIds: { gbraid: "paid-gbraid" },
|
|
155
|
+
conversionValueInUSDCents: 12999,
|
|
156
|
+
}),
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
expect(result.permanentFailures.size).toBe(0);
|
|
160
|
+
expect(postSpy).toHaveBeenCalledTimes(2);
|
|
161
|
+
expect(postSpy.mock.calls[0]?.[0]).toBe(
|
|
162
|
+
"https://oauth2.googleapis.com/token",
|
|
163
|
+
);
|
|
164
|
+
expect(postSpy.mock.calls[0]?.[1]).toContain(
|
|
165
|
+
"refresh_token=oauth-refresh-token",
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const uploadUrl: string = postSpy.mock.calls[1]?.[0] as string;
|
|
169
|
+
const uploadBody: JSONObject = postSpy.mock.calls[1]?.[1] as JSONObject;
|
|
170
|
+
const uploadOptions: JSONObject = postSpy.mock.calls[1]?.[2] as JSONObject;
|
|
171
|
+
const conversions: Array<JSONObject> = uploadBody[
|
|
172
|
+
"conversions"
|
|
173
|
+
] as Array<JSONObject>;
|
|
174
|
+
|
|
175
|
+
expect(uploadUrl).toBe(
|
|
176
|
+
"https://googleads.googleapis.com/v23/customers/1234567890:uploadClickConversions",
|
|
177
|
+
);
|
|
178
|
+
expect(uploadBody["partialFailure"]).toBe(true);
|
|
179
|
+
expect(conversions[0]).toEqual({
|
|
180
|
+
wbraid: "signup-wbraid",
|
|
181
|
+
conversionAction: "customers/1234567890/conversionActions/111111",
|
|
182
|
+
conversionDateTime: "2026-07-22 10:11:12+00:00",
|
|
183
|
+
});
|
|
184
|
+
expect(conversions[1]).toEqual({
|
|
185
|
+
gbraid: "paid-gbraid",
|
|
186
|
+
conversionAction: "customers/1234567890/conversionActions/222222",
|
|
187
|
+
conversionDateTime: "2026-07-22 10:11:12+00:00",
|
|
188
|
+
conversionValue: 129.99,
|
|
189
|
+
currencyCode: "USD",
|
|
190
|
+
});
|
|
191
|
+
expect(uploadOptions).toMatchObject({
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: "Bearer access-token",
|
|
194
|
+
"developer-token": "developer-token",
|
|
195
|
+
"login-customer-id": "0987654321",
|
|
196
|
+
},
|
|
197
|
+
timeout: 30000,
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("does not send zero or negative conversion values", async () => {
|
|
202
|
+
postSpy
|
|
203
|
+
.mockResolvedValueOnce(
|
|
204
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
205
|
+
)
|
|
206
|
+
.mockResolvedValueOnce(response({}) as never);
|
|
207
|
+
|
|
208
|
+
await provider.upload([
|
|
209
|
+
makeConversion({ conversionValueInUSDCents: 0 }),
|
|
210
|
+
makeConversion({ conversionValueInUSDCents: -100 }),
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
const uploadBody: JSONObject = postSpy.mock.calls[1]?.[1] as JSONObject;
|
|
214
|
+
const conversions: Array<JSONObject> = uploadBody[
|
|
215
|
+
"conversions"
|
|
216
|
+
] as Array<JSONObject>;
|
|
217
|
+
|
|
218
|
+
expect(conversions[0]?.["conversionValue"]).toBeUndefined();
|
|
219
|
+
expect(conversions[0]?.["currencyCode"]).toBeUndefined();
|
|
220
|
+
expect(conversions[1]?.["conversionValue"]).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("reuses an unexpired OAuth access token across batches", async () => {
|
|
224
|
+
postSpy
|
|
225
|
+
.mockResolvedValueOnce(
|
|
226
|
+
response({ access_token: "cached-token", expires_in: 3600 }) as never,
|
|
227
|
+
)
|
|
228
|
+
.mockResolvedValue(response({}) as never);
|
|
229
|
+
|
|
230
|
+
await provider.upload([makeConversion()]);
|
|
231
|
+
await provider.upload([makeConversion({ clickIds: { gclid: "second" } })]);
|
|
232
|
+
|
|
233
|
+
expect(postSpy).toHaveBeenCalledTimes(3);
|
|
234
|
+
expect(
|
|
235
|
+
postSpy.mock.calls.filter((call: Array<unknown>) => {
|
|
236
|
+
return call[0] === "https://oauth2.googleapis.com/token";
|
|
237
|
+
}),
|
|
238
|
+
).toHaveLength(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("maps indexed partial failures to the submitted conversion", async () => {
|
|
242
|
+
postSpy
|
|
243
|
+
.mockResolvedValueOnce(
|
|
244
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
245
|
+
)
|
|
246
|
+
.mockResolvedValueOnce(
|
|
247
|
+
response({
|
|
248
|
+
partialFailureError: {
|
|
249
|
+
details: [
|
|
250
|
+
{
|
|
251
|
+
errors: [
|
|
252
|
+
{
|
|
253
|
+
message: "Expired click identifier",
|
|
254
|
+
location: {
|
|
255
|
+
fieldPathElements: [
|
|
256
|
+
{ fieldName: "conversions", index: 1 },
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
}) as never,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const result: ConversionUploadBatchResult = await provider.upload([
|
|
268
|
+
makeConversion(),
|
|
269
|
+
makeConversion({ clickIds: { gclid: "expired" } }),
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
expect(Array.from(result.permanentFailures.entries())).toEqual([
|
|
273
|
+
[1, "Expired click identifier"],
|
|
274
|
+
]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("treats ALREADY_EXISTS as successful idempotent delivery", async () => {
|
|
278
|
+
postSpy
|
|
279
|
+
.mockResolvedValueOnce(
|
|
280
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
281
|
+
)
|
|
282
|
+
.mockResolvedValueOnce(
|
|
283
|
+
response({
|
|
284
|
+
partialFailureError: {
|
|
285
|
+
details: [
|
|
286
|
+
{
|
|
287
|
+
errors: [
|
|
288
|
+
{
|
|
289
|
+
errorCode: { conversionUploadError: "ALREADY_EXISTS" },
|
|
290
|
+
message: "The conversion was already uploaded",
|
|
291
|
+
location: {
|
|
292
|
+
fieldPathElements: [
|
|
293
|
+
{ fieldName: "conversions", index: 0 },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
}) as never,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const result: ConversionUploadBatchResult = await provider.upload([
|
|
305
|
+
makeConversion(),
|
|
306
|
+
]);
|
|
307
|
+
|
|
308
|
+
expect(result.permanentFailures.size).toBe(0);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("logs an unparseable partial failure instead of silently hiding it", async () => {
|
|
312
|
+
const errorSpy: SpyInstance<typeof logger.error> = jest
|
|
313
|
+
.spyOn(logger, "error")
|
|
314
|
+
.mockImplementation((): void => {
|
|
315
|
+
return undefined;
|
|
316
|
+
});
|
|
317
|
+
postSpy
|
|
318
|
+
.mockResolvedValueOnce(
|
|
319
|
+
response({ access_token: "access-token", expires_in: 3600 }) as never,
|
|
320
|
+
)
|
|
321
|
+
.mockResolvedValueOnce(
|
|
322
|
+
response({
|
|
323
|
+
partialFailureError: {
|
|
324
|
+
details: [{ errors: [{ message: "Unindexed failure" }] }],
|
|
325
|
+
},
|
|
326
|
+
}) as never,
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const result: ConversionUploadBatchResult = await provider.upload([
|
|
330
|
+
makeConversion(),
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
expect(result.permanentFailures.size).toBe(0);
|
|
334
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
335
|
+
expect.stringContaining(
|
|
336
|
+
"Google Ads partial failure with unparseable indexes",
|
|
337
|
+
),
|
|
338
|
+
);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import ExceptionMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/ExceptionMonitorCriteria";
|
|
2
|
+
import {
|
|
3
|
+
CheckOn,
|
|
4
|
+
CriteriaFilter,
|
|
5
|
+
FilterType,
|
|
6
|
+
} from "../../../../../Types/Monitor/CriteriaFilter";
|
|
7
|
+
import ExceptionMonitorResponse from "../../../../../Types/Monitor/ExceptionMonitor/ExceptionMonitorResponse";
|
|
8
|
+
import DataToProcess from "../../../../../Server/Utils/Monitor/DataToProcess";
|
|
9
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* ExceptionMonitorCriteria is the ExceptionCount analogue of
|
|
13
|
+
* LogMonitorCriteria and shares the same fallback-fixed worker path
|
|
14
|
+
* (monitorException). Covered here for parity across the count-based
|
|
15
|
+
* telemetry criteria.
|
|
16
|
+
*/
|
|
17
|
+
function buildResponse(exceptionCount: number | undefined): DataToProcess {
|
|
18
|
+
const response: Partial<ExceptionMonitorResponse> = {
|
|
19
|
+
projectId: ObjectID.generate(),
|
|
20
|
+
monitorId: ObjectID.generate(),
|
|
21
|
+
exceptionCount: exceptionCount as number,
|
|
22
|
+
exceptionQuery: {},
|
|
23
|
+
};
|
|
24
|
+
return response as DataToProcess;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function evaluate(
|
|
28
|
+
exceptionCount: number | undefined,
|
|
29
|
+
criteriaFilter: CriteriaFilter,
|
|
30
|
+
): Promise<string | null> {
|
|
31
|
+
return ExceptionMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
32
|
+
dataToProcess: buildResponse(exceptionCount),
|
|
33
|
+
criteriaFilter,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("ExceptionMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
|
|
38
|
+
test("ExceptionCount GreaterThan 0 → met when exceptions exist", async () => {
|
|
39
|
+
expect(
|
|
40
|
+
await evaluate(3, {
|
|
41
|
+
checkOn: CheckOn.ExceptionCount,
|
|
42
|
+
filterType: FilterType.GreaterThan,
|
|
43
|
+
value: 0,
|
|
44
|
+
}),
|
|
45
|
+
).toBeTruthy();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("ExceptionCount EqualTo 0 → met when there are none (default)", async () => {
|
|
49
|
+
expect(
|
|
50
|
+
await evaluate(0, {
|
|
51
|
+
checkOn: CheckOn.ExceptionCount,
|
|
52
|
+
filterType: FilterType.EqualTo,
|
|
53
|
+
value: 0,
|
|
54
|
+
}),
|
|
55
|
+
).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("ExceptionCount GreaterThan → not met when none", async () => {
|
|
59
|
+
expect(
|
|
60
|
+
await evaluate(0, {
|
|
61
|
+
checkOn: CheckOn.ExceptionCount,
|
|
62
|
+
filterType: FilterType.GreaterThan,
|
|
63
|
+
value: 0,
|
|
64
|
+
}),
|
|
65
|
+
).toBeNull();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a missing exceptionCount is treated as 0", async () => {
|
|
69
|
+
expect(
|
|
70
|
+
await evaluate(undefined, {
|
|
71
|
+
checkOn: CheckOn.ExceptionCount,
|
|
72
|
+
filterType: FilterType.EqualTo,
|
|
73
|
+
value: 0,
|
|
74
|
+
}),
|
|
75
|
+
).toBeTruthy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("a non-ExceptionCount checkOn returns null (unhandled)", async () => {
|
|
79
|
+
expect(
|
|
80
|
+
await evaluate(9, {
|
|
81
|
+
checkOn: CheckOn.LogCount,
|
|
82
|
+
filterType: FilterType.GreaterThan,
|
|
83
|
+
value: 0,
|
|
84
|
+
}),
|
|
85
|
+
).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|