@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,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
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import LogMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/LogMonitorCriteria";
|
|
2
|
+
import {
|
|
3
|
+
CheckOn,
|
|
4
|
+
CriteriaFilter,
|
|
5
|
+
FilterType,
|
|
6
|
+
} from "../../../../../Types/Monitor/CriteriaFilter";
|
|
7
|
+
import LogMonitorResponse from "../../../../../Types/Monitor/LogMonitor/LogMonitorResponse";
|
|
8
|
+
import DataToProcess from "../../../../../Server/Utils/Monitor/DataToProcess";
|
|
9
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* LogMonitorCriteria is what turns the logCount the (now-fixed) worker
|
|
13
|
+
* produces into an online/offline verdict. It only understands
|
|
14
|
+
* CheckOn.LogCount — the sole check the Logs criteria UI offers — and defers
|
|
15
|
+
* the numeric comparison to CompareCriteria.
|
|
16
|
+
*/
|
|
17
|
+
function buildResponse(logCount: number | undefined): DataToProcess {
|
|
18
|
+
const response: Partial<LogMonitorResponse> = {
|
|
19
|
+
projectId: ObjectID.generate(),
|
|
20
|
+
monitorId: ObjectID.generate(),
|
|
21
|
+
logCount: logCount as number,
|
|
22
|
+
logQuery: {},
|
|
23
|
+
};
|
|
24
|
+
return response as DataToProcess;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function evaluate(
|
|
28
|
+
logCount: number | undefined,
|
|
29
|
+
criteriaFilter: CriteriaFilter,
|
|
30
|
+
): Promise<string | null> {
|
|
31
|
+
return LogMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
32
|
+
dataToProcess: buildResponse(logCount),
|
|
33
|
+
criteriaFilter,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("LogMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
|
|
38
|
+
describe("LogCount GreaterThan", () => {
|
|
39
|
+
test("count above threshold → met", async () => {
|
|
40
|
+
const result: string | null = await evaluate(5, {
|
|
41
|
+
checkOn: CheckOn.LogCount,
|
|
42
|
+
filterType: FilterType.GreaterThan,
|
|
43
|
+
value: 0,
|
|
44
|
+
});
|
|
45
|
+
expect(result).toBeTruthy();
|
|
46
|
+
expect(result).toContain("Log Count");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("count equal to threshold → not met", async () => {
|
|
50
|
+
expect(
|
|
51
|
+
await evaluate(3, {
|
|
52
|
+
checkOn: CheckOn.LogCount,
|
|
53
|
+
filterType: FilterType.GreaterThan,
|
|
54
|
+
value: 3,
|
|
55
|
+
}),
|
|
56
|
+
).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("count below threshold → not met", async () => {
|
|
60
|
+
expect(
|
|
61
|
+
await evaluate(0, {
|
|
62
|
+
checkOn: CheckOn.LogCount,
|
|
63
|
+
filterType: FilterType.GreaterThan,
|
|
64
|
+
value: 0,
|
|
65
|
+
}),
|
|
66
|
+
).toBeNull();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe("LogCount EqualTo (the default offline criteria)", () => {
|
|
71
|
+
test("zero logs equal to 0 → met (monitor goes offline)", async () => {
|
|
72
|
+
const result: string | null = await evaluate(0, {
|
|
73
|
+
checkOn: CheckOn.LogCount,
|
|
74
|
+
filterType: FilterType.EqualTo,
|
|
75
|
+
value: 0,
|
|
76
|
+
});
|
|
77
|
+
expect(result).toBeTruthy();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("some logs not equal to 0 → not met", async () => {
|
|
81
|
+
expect(
|
|
82
|
+
await evaluate(7, {
|
|
83
|
+
checkOn: CheckOn.LogCount,
|
|
84
|
+
filterType: FilterType.EqualTo,
|
|
85
|
+
value: 0,
|
|
86
|
+
}),
|
|
87
|
+
).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe("other numeric comparators", () => {
|
|
92
|
+
test("LessThan → met when below", async () => {
|
|
93
|
+
expect(
|
|
94
|
+
await evaluate(2, {
|
|
95
|
+
checkOn: CheckOn.LogCount,
|
|
96
|
+
filterType: FilterType.LessThan,
|
|
97
|
+
value: 5,
|
|
98
|
+
}),
|
|
99
|
+
).toBeTruthy();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("GreaterThanOrEqualTo → met at the boundary", async () => {
|
|
103
|
+
expect(
|
|
104
|
+
await evaluate(5, {
|
|
105
|
+
checkOn: CheckOn.LogCount,
|
|
106
|
+
filterType: FilterType.GreaterThanOrEqualTo,
|
|
107
|
+
value: 5,
|
|
108
|
+
}),
|
|
109
|
+
).toBeTruthy();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("LessThanOrEqualTo → met at the boundary", async () => {
|
|
113
|
+
expect(
|
|
114
|
+
await evaluate(5, {
|
|
115
|
+
checkOn: CheckOn.LogCount,
|
|
116
|
+
filterType: FilterType.LessThanOrEqualTo,
|
|
117
|
+
value: 5,
|
|
118
|
+
}),
|
|
119
|
+
).toBeTruthy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("NotEqualTo → met when different", async () => {
|
|
123
|
+
expect(
|
|
124
|
+
await evaluate(4, {
|
|
125
|
+
checkOn: CheckOn.LogCount,
|
|
126
|
+
filterType: FilterType.NotEqualTo,
|
|
127
|
+
value: 0,
|
|
128
|
+
}),
|
|
129
|
+
).toBeTruthy();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe("edge cases", () => {
|
|
134
|
+
test("a missing logCount is treated as 0", async () => {
|
|
135
|
+
// undefined logCount → 0; "equal to 0" is therefore met.
|
|
136
|
+
expect(
|
|
137
|
+
await evaluate(undefined, {
|
|
138
|
+
checkOn: CheckOn.LogCount,
|
|
139
|
+
filterType: FilterType.EqualTo,
|
|
140
|
+
value: 0,
|
|
141
|
+
}),
|
|
142
|
+
).toBeTruthy();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a string threshold is coerced to a number", async () => {
|
|
146
|
+
expect(
|
|
147
|
+
await evaluate(10, {
|
|
148
|
+
checkOn: CheckOn.LogCount,
|
|
149
|
+
filterType: FilterType.GreaterThan,
|
|
150
|
+
value: "5",
|
|
151
|
+
}),
|
|
152
|
+
).toBeTruthy();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a non-LogCount checkOn returns null (unhandled)", async () => {
|
|
156
|
+
expect(
|
|
157
|
+
await evaluate(100, {
|
|
158
|
+
checkOn: CheckOn.SpanCount,
|
|
159
|
+
filterType: FilterType.GreaterThan,
|
|
160
|
+
value: 0,
|
|
161
|
+
}),
|
|
162
|
+
).toBeNull();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import TraceMonitorCriteria from "../../../../../Server/Utils/Monitor/Criteria/TraceMonitorCriteria";
|
|
2
|
+
import {
|
|
3
|
+
CheckOn,
|
|
4
|
+
CriteriaFilter,
|
|
5
|
+
FilterType,
|
|
6
|
+
} from "../../../../../Types/Monitor/CriteriaFilter";
|
|
7
|
+
import TraceMonitorResponse from "../../../../../Types/Monitor/TraceMonitor/TraceMonitorResponse";
|
|
8
|
+
import DataToProcess from "../../../../../Server/Utils/Monitor/DataToProcess";
|
|
9
|
+
import ObjectID from "../../../../../Types/ObjectID";
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* TraceMonitorCriteria is the SpanCount analogue of LogMonitorCriteria and
|
|
13
|
+
* shares the same fallback-fixed worker path (monitorTrace). Covered here for
|
|
14
|
+
* parity so the two count-based telemetry criteria stay in lock-step.
|
|
15
|
+
*/
|
|
16
|
+
function buildResponse(spanCount: number | undefined): DataToProcess {
|
|
17
|
+
const response: Partial<TraceMonitorResponse> = {
|
|
18
|
+
projectId: ObjectID.generate(),
|
|
19
|
+
monitorId: ObjectID.generate(),
|
|
20
|
+
spanCount: spanCount as number,
|
|
21
|
+
spanQuery: {},
|
|
22
|
+
};
|
|
23
|
+
return response as DataToProcess;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function evaluate(
|
|
27
|
+
spanCount: number | undefined,
|
|
28
|
+
criteriaFilter: CriteriaFilter,
|
|
29
|
+
): Promise<string | null> {
|
|
30
|
+
return TraceMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
|
|
31
|
+
dataToProcess: buildResponse(spanCount),
|
|
32
|
+
criteriaFilter,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("TraceMonitorCriteria.isMonitorInstanceCriteriaFilterMet", () => {
|
|
37
|
+
test("SpanCount GreaterThan → met when above threshold", async () => {
|
|
38
|
+
expect(
|
|
39
|
+
await evaluate(9, {
|
|
40
|
+
checkOn: CheckOn.SpanCount,
|
|
41
|
+
filterType: FilterType.GreaterThan,
|
|
42
|
+
value: 0,
|
|
43
|
+
}),
|
|
44
|
+
).toBeTruthy();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("SpanCount EqualTo 0 → met when no spans (offline)", async () => {
|
|
48
|
+
expect(
|
|
49
|
+
await evaluate(0, {
|
|
50
|
+
checkOn: CheckOn.SpanCount,
|
|
51
|
+
filterType: FilterType.EqualTo,
|
|
52
|
+
value: 0,
|
|
53
|
+
}),
|
|
54
|
+
).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("SpanCount GreaterThan → not met at/below threshold", async () => {
|
|
58
|
+
expect(
|
|
59
|
+
await evaluate(0, {
|
|
60
|
+
checkOn: CheckOn.SpanCount,
|
|
61
|
+
filterType: FilterType.GreaterThan,
|
|
62
|
+
value: 0,
|
|
63
|
+
}),
|
|
64
|
+
).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a missing spanCount is treated as 0", async () => {
|
|
68
|
+
expect(
|
|
69
|
+
await evaluate(undefined, {
|
|
70
|
+
checkOn: CheckOn.SpanCount,
|
|
71
|
+
filterType: FilterType.EqualTo,
|
|
72
|
+
value: 0,
|
|
73
|
+
}),
|
|
74
|
+
).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a non-SpanCount checkOn returns null (unhandled)", async () => {
|
|
78
|
+
expect(
|
|
79
|
+
await evaluate(50, {
|
|
80
|
+
checkOn: CheckOn.LogCount,
|
|
81
|
+
filterType: FilterType.GreaterThan,
|
|
82
|
+
value: 0,
|
|
83
|
+
}),
|
|
84
|
+
).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|