@oneuptime/common 12.0.19 → 12.0.21
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/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { AddUserSlackAndMicrosoftTeams1789000000000 } from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams";
|
|
2
|
+
import SchemaMigrations from "../../../../Server/Infrastructure/Postgres/SchemaMigrations/Index";
|
|
3
|
+
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { describe, expect, test } from "@jest/globals";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* The guard that lets this migration run twice.
|
|
10
|
+
*
|
|
11
|
+
* It shipped as AddUserSlackAndMicrosoftTeams1788800000000 and was renumbered
|
|
12
|
+
* to 1789000000000 to clear a timestamp collision. TypeORM records a migration
|
|
13
|
+
* by class NAME, so the rename made it pending again on every database that
|
|
14
|
+
* had already run it, and `CREATE TABLE "UserSlack"` failed on its own table —
|
|
15
|
+
* which stops the runner, leaving this and every later migration pending on
|
|
16
|
+
* every boot until it is fixed.
|
|
17
|
+
*
|
|
18
|
+
* So there are two databases to satisfy, and both are asserted below: one that
|
|
19
|
+
* has never seen this migration must get the whole schema, and one that
|
|
20
|
+
* already carries it must get no DDL at all.
|
|
21
|
+
*
|
|
22
|
+
* The statements are inspected rather than executed — these tests run without
|
|
23
|
+
* a database, and what can go wrong here is which statements are issued, not
|
|
24
|
+
* whether Postgres understands them.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const MIGRATION_NAME: string = "AddUserSlackAndMicrosoftTeams1789000000000";
|
|
28
|
+
|
|
29
|
+
const SUPERSEDED_MIGRATION_NAME: string =
|
|
30
|
+
"AddUserSlackAndMicrosoftTeams1788800000000";
|
|
31
|
+
|
|
32
|
+
const MIGRATION_TIMESTAMP: string = "1789000000000";
|
|
33
|
+
|
|
34
|
+
const MIGRATION_DIRECTORY: string = path.join(
|
|
35
|
+
__dirname,
|
|
36
|
+
"../../../../Server/Infrastructure/Postgres/SchemaMigrations",
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* Every statement `up` issues after the probe. The count is asserted rather
|
|
41
|
+
* than sampled: the risk the guard introduces is an early return that swallows
|
|
42
|
+
* work a fresh install needs, and only a total catches a statement going
|
|
43
|
+
* missing from the middle.
|
|
44
|
+
*/
|
|
45
|
+
const STATEMENTS_IN_UP: number = 30;
|
|
46
|
+
|
|
47
|
+
type CapturedQueryRunner = {
|
|
48
|
+
runner: QueryRunner;
|
|
49
|
+
queries: Array<string>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type IsProbeFunction = (sql: string) => boolean;
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* The one read `up` makes: does "UserSlack" already exist? Matched on shape
|
|
56
|
+
* rather than on the exact string so reformatting the SQL does not fail these.
|
|
57
|
+
*/
|
|
58
|
+
const isProbe: IsProbeFunction = (sql: string): boolean => {
|
|
59
|
+
return (
|
|
60
|
+
sql.includes("information_schema.tables") && sql.includes("'UserSlack'")
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type MockQueryRunnerFunction = (
|
|
65
|
+
userSlackTableExists: boolean,
|
|
66
|
+
) => CapturedQueryRunner;
|
|
67
|
+
|
|
68
|
+
const mockQueryRunner: MockQueryRunnerFunction = (
|
|
69
|
+
userSlackTableExists: boolean,
|
|
70
|
+
): CapturedQueryRunner => {
|
|
71
|
+
const queries: Array<string> = [];
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
queries: queries,
|
|
75
|
+
runner: {
|
|
76
|
+
query: async (sql: string): Promise<unknown> => {
|
|
77
|
+
queries.push(sql);
|
|
78
|
+
|
|
79
|
+
if (isProbe(sql)) {
|
|
80
|
+
return [{ exists: userSlackTableExists }];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return undefined;
|
|
84
|
+
},
|
|
85
|
+
} as unknown as QueryRunner,
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const migration: AddUserSlackAndMicrosoftTeams1789000000000 =
|
|
90
|
+
new AddUserSlackAndMicrosoftTeams1789000000000();
|
|
91
|
+
|
|
92
|
+
describe("AddUserSlackAndMicrosoftTeams migration", () => {
|
|
93
|
+
/*
|
|
94
|
+
* TypeORM matches migrations by this name. A mismatch between the class name
|
|
95
|
+
* and the `name` property makes the migration run again on every boot.
|
|
96
|
+
*/
|
|
97
|
+
test("names itself consistently so it is recorded as run", () => {
|
|
98
|
+
expect(migration.name).toBe(MIGRATION_NAME);
|
|
99
|
+
expect(AddUserSlackAndMicrosoftTeams1789000000000.name).toBe(
|
|
100
|
+
MIGRATION_NAME,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("registration", () => {
|
|
105
|
+
test("it is registered exactly once", () => {
|
|
106
|
+
const occurrences: number = SchemaMigrations.filter(
|
|
107
|
+
(registered: new () => MigrationInterface): boolean => {
|
|
108
|
+
return registered.name === MIGRATION_NAME;
|
|
109
|
+
},
|
|
110
|
+
).length;
|
|
111
|
+
|
|
112
|
+
expect(occurrences).toBe(1);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
* The old number belongs to DropMarketingConversionAddEnterpriseLicenseEmail
|
|
117
|
+
* now. Registering both names would run the same DDL twice under two
|
|
118
|
+
* records, which is the failure this migration already had once.
|
|
119
|
+
*/
|
|
120
|
+
test("no longer registers the timestamp it was renamed away from", () => {
|
|
121
|
+
const names: Array<string> = SchemaMigrations.map(
|
|
122
|
+
(registered: new () => MigrationInterface): string => {
|
|
123
|
+
return registered.name;
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(names).not.toContain(SUPERSEDED_MIGRATION_NAME);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("exactly one file on disk carries its timestamp, and it is this one", () => {
|
|
131
|
+
const matching: Array<string> = fs
|
|
132
|
+
.readdirSync(MIGRATION_DIRECTORY)
|
|
133
|
+
.filter((file: string): boolean => {
|
|
134
|
+
return file.startsWith(`${MIGRATION_TIMESTAMP}-`);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(matching).toEqual([
|
|
138
|
+
`${MIGRATION_TIMESTAMP}-AddUserSlackAndMicrosoftTeams.ts`,
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("on a database that has never run it", () => {
|
|
144
|
+
test("checks for the table before touching anything", async () => {
|
|
145
|
+
const up: CapturedQueryRunner = mockQueryRunner(false);
|
|
146
|
+
|
|
147
|
+
await migration.up(up.runner);
|
|
148
|
+
|
|
149
|
+
expect(isProbe(up.queries[0]!)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("creates the whole schema, unchanged by the guard", async () => {
|
|
153
|
+
const up: CapturedQueryRunner = mockQueryRunner(false);
|
|
154
|
+
|
|
155
|
+
await migration.up(up.runner);
|
|
156
|
+
|
|
157
|
+
const statements: Array<string> = up.queries.filter(
|
|
158
|
+
(sql: string): boolean => {
|
|
159
|
+
return !isProbe(sql);
|
|
160
|
+
},
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
expect(statements.length).toBe(STATEMENTS_IN_UP);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("creates both method tables and their notification wiring", async () => {
|
|
167
|
+
const up: CapturedQueryRunner = mockQueryRunner(false);
|
|
168
|
+
|
|
169
|
+
await migration.up(up.runner);
|
|
170
|
+
|
|
171
|
+
const sql: string = up.queries.join("\n");
|
|
172
|
+
|
|
173
|
+
expect(sql).toContain(`CREATE TABLE "UserSlack"`);
|
|
174
|
+
expect(sql).toContain(`CREATE TABLE "UserMicrosoftTeams"`);
|
|
175
|
+
expect(sql).toContain(
|
|
176
|
+
`ALTER TABLE "UserNotificationRule" ADD "userSlackId" uuid`,
|
|
177
|
+
);
|
|
178
|
+
expect(sql).toContain(
|
|
179
|
+
`ALTER TABLE "UserNotificationRule" ADD "userMicrosoftTeamsId" uuid`,
|
|
180
|
+
);
|
|
181
|
+
expect(sql).toContain(
|
|
182
|
+
`ALTER TABLE "UserNotificationSetting" ADD "alertBySlack" boolean NOT NULL DEFAULT false`,
|
|
183
|
+
);
|
|
184
|
+
expect(sql).toContain(
|
|
185
|
+
`ALTER TABLE "UserNotificationSetting" ADD "alertByMicrosoftTeams" boolean NOT NULL DEFAULT false`,
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("on a database that already ran it under its old name", () => {
|
|
191
|
+
/*
|
|
192
|
+
* The whole point. Migrations run one transaction each, so a recorded run
|
|
193
|
+
* committed all 30 statements or none of them — the table being there means
|
|
194
|
+
* the schema is there, and re-issuing any of it can only fail.
|
|
195
|
+
*/
|
|
196
|
+
test("issues nothing but the probe", async () => {
|
|
197
|
+
const up: CapturedQueryRunner = mockQueryRunner(true);
|
|
198
|
+
|
|
199
|
+
await migration.up(up.runner);
|
|
200
|
+
|
|
201
|
+
expect(up.queries.length).toBe(1);
|
|
202
|
+
expect(isProbe(up.queries[0]!)).toBe(true);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("never re-runs the CREATE TABLE that was failing", async () => {
|
|
206
|
+
const up: CapturedQueryRunner = mockQueryRunner(true);
|
|
207
|
+
|
|
208
|
+
await migration.up(up.runner);
|
|
209
|
+
|
|
210
|
+
const sql: string = up.queries.join("\n");
|
|
211
|
+
|
|
212
|
+
expect(sql).not.toContain(`CREATE TABLE "UserSlack"`);
|
|
213
|
+
expect(sql).not.toContain(`CREATE TABLE "UserMicrosoftTeams"`);
|
|
214
|
+
expect(sql).not.toContain("ADD CONSTRAINT");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* `down` is deliberately NOT guarded. It reverses the schema whichever name
|
|
220
|
+
* recorded it, and a rollback that finds the tables missing has bigger
|
|
221
|
+
* problems than a failed DROP.
|
|
222
|
+
*/
|
|
223
|
+
describe("down", () => {
|
|
224
|
+
test("drops both method tables", async () => {
|
|
225
|
+
const down: CapturedQueryRunner = mockQueryRunner(true);
|
|
226
|
+
|
|
227
|
+
await migration.down(down.runner);
|
|
228
|
+
|
|
229
|
+
const sql: string = down.queries.join("\n");
|
|
230
|
+
|
|
231
|
+
expect(sql).toContain(`DROP TABLE "UserSlack"`);
|
|
232
|
+
expect(sql).toContain(`DROP TABLE "UserMicrosoftTeams"`);
|
|
233
|
+
expect(down.queries.length).toBe(STATEMENTS_IN_UP);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -7,6 +7,9 @@ import SubscriptionStatus from "../../../Types/Billing/SubscriptionStatus";
|
|
|
7
7
|
import OneUptimeDate from "../../../Types/Date";
|
|
8
8
|
import ObjectID from "../../../Types/ObjectID";
|
|
9
9
|
import logger from "../../../Server/Utils/Logger";
|
|
10
|
+
import MarketingEventUtil from "../../../Server/Utils/Marketing/MarketingEventUtil";
|
|
11
|
+
import ProductAnalytics from "../../../Server/Utils/ProductAnalytics";
|
|
12
|
+
import { MarketingEvent } from "../../../Types/Marketing/MarketingEvent";
|
|
10
13
|
import { describe, expect, it, afterEach } from "@jest/globals";
|
|
11
14
|
|
|
12
15
|
/*
|
|
@@ -647,4 +650,139 @@ describe("ProjectService.changePlan", () => {
|
|
|
647
650
|
});
|
|
648
651
|
});
|
|
649
652
|
});
|
|
653
|
+
|
|
654
|
+
/*
|
|
655
|
+
* A first subscription is now its own conversion (subscription_started,
|
|
656
|
+
* raised from the create path). The two must not bleed into each other:
|
|
657
|
+
* net-new revenue and expansion revenue are different numbers, and once one
|
|
658
|
+
* is reported as the other nothing downstream can separate them again.
|
|
659
|
+
*
|
|
660
|
+
* This half of the pair guards the direction it moves in: a genuine tier
|
|
661
|
+
* movement out of an existing plan is still an upgrade, and must not start
|
|
662
|
+
* arriving as a first subscription.
|
|
663
|
+
*/
|
|
664
|
+
describe("the conversion a real upgrade emits", () => {
|
|
665
|
+
const OLD_PLAN_ID: string = "price_monthly_growth";
|
|
666
|
+
|
|
667
|
+
it("should emit subscription_upgraded, and not subscription_started", async () => {
|
|
668
|
+
setup({
|
|
669
|
+
project: fakeProject({
|
|
670
|
+
paymentProviderPlanId: OLD_PLAN_ID,
|
|
671
|
+
createdOwnerEmail: "owner@example.com",
|
|
672
|
+
}),
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
/*
|
|
676
|
+
* Direction is decided by plan ORDER, so the two plans have to be told
|
|
677
|
+
* apart: Growth at 2, Scale at 3.
|
|
678
|
+
*/
|
|
679
|
+
(
|
|
680
|
+
SubscriptionPlan.getSubscriptionPlanById as unknown as jest.SpyInstance
|
|
681
|
+
).mockImplementation((planId: string) => {
|
|
682
|
+
if (planId === OLD_PLAN_ID) {
|
|
683
|
+
return new SubscriptionPlan(
|
|
684
|
+
OLD_PLAN_ID,
|
|
685
|
+
"price_yearly_growth",
|
|
686
|
+
"Growth",
|
|
687
|
+
49,
|
|
688
|
+
45,
|
|
689
|
+
2,
|
|
690
|
+
14,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return new SubscriptionPlan(
|
|
695
|
+
NEW_PLAN_ID,
|
|
696
|
+
"price_yearly_scale",
|
|
697
|
+
"Scale",
|
|
698
|
+
99,
|
|
699
|
+
84,
|
|
700
|
+
3,
|
|
701
|
+
0,
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
jest.spyOn(ProductAnalytics, "capture").mockReturnValue(undefined);
|
|
706
|
+
|
|
707
|
+
const emitInBackground: jest.SpyInstance = jest
|
|
708
|
+
.spyOn(MarketingEventUtil, "emitInBackground")
|
|
709
|
+
.mockReturnValue(undefined);
|
|
710
|
+
|
|
711
|
+
await ProjectService.changePlan({
|
|
712
|
+
projectId: PROJECT_ID,
|
|
713
|
+
paymentProviderPlanId: NEW_PLAN_ID,
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
const emittedTypes: Array<string> = emitInBackground.mock.calls.map(
|
|
717
|
+
(call: Array<unknown>) => {
|
|
718
|
+
return (call[0] as MarketingEvent).eventType as string;
|
|
719
|
+
},
|
|
720
|
+
);
|
|
721
|
+
|
|
722
|
+
expect(emittedTypes).toEqual(["subscription_upgraded"]);
|
|
723
|
+
expect(emittedTypes).not.toContain("subscription_started");
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should keep the instant in the upgrade's event id, unlike a first subscription", async () => {
|
|
727
|
+
/*
|
|
728
|
+
* A project can upgrade, downgrade and upgrade again, so there is no
|
|
729
|
+
* naturally unique key here and the instant is what separates one change
|
|
730
|
+
* from the next. subscription_started is the opposite case - it has no
|
|
731
|
+
* next one, so it carries no timestamp - and the ids must not converge.
|
|
732
|
+
*/
|
|
733
|
+
setup({
|
|
734
|
+
project: fakeProject({
|
|
735
|
+
paymentProviderPlanId: OLD_PLAN_ID,
|
|
736
|
+
createdOwnerEmail: "owner@example.com",
|
|
737
|
+
}),
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
(
|
|
741
|
+
SubscriptionPlan.getSubscriptionPlanById as unknown as jest.SpyInstance
|
|
742
|
+
).mockImplementation((planId: string) => {
|
|
743
|
+
if (planId === OLD_PLAN_ID) {
|
|
744
|
+
return new SubscriptionPlan(
|
|
745
|
+
OLD_PLAN_ID,
|
|
746
|
+
"price_yearly_growth",
|
|
747
|
+
"Growth",
|
|
748
|
+
49,
|
|
749
|
+
45,
|
|
750
|
+
2,
|
|
751
|
+
14,
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return new SubscriptionPlan(
|
|
756
|
+
NEW_PLAN_ID,
|
|
757
|
+
"price_yearly_scale",
|
|
758
|
+
"Scale",
|
|
759
|
+
99,
|
|
760
|
+
84,
|
|
761
|
+
3,
|
|
762
|
+
0,
|
|
763
|
+
);
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
jest.spyOn(ProductAnalytics, "capture").mockReturnValue(undefined);
|
|
767
|
+
|
|
768
|
+
const emitInBackground: jest.SpyInstance = jest
|
|
769
|
+
.spyOn(MarketingEventUtil, "emitInBackground")
|
|
770
|
+
.mockReturnValue(undefined);
|
|
771
|
+
|
|
772
|
+
await ProjectService.changePlan({
|
|
773
|
+
projectId: PROJECT_ID,
|
|
774
|
+
paymentProviderPlanId: NEW_PLAN_ID,
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
const event: MarketingEvent = emitInBackground.mock
|
|
778
|
+
.calls[0]![0] as MarketingEvent;
|
|
779
|
+
|
|
780
|
+
expect(event.eventId).toBe(
|
|
781
|
+
`subscription_upgraded:${PROJECT_ID.toString()}:${event.occurredAt}`,
|
|
782
|
+
);
|
|
783
|
+
expect(event.eventId).not.toBe(
|
|
784
|
+
`subscription_started:${PROJECT_ID.toString()}`,
|
|
785
|
+
);
|
|
786
|
+
});
|
|
787
|
+
});
|
|
650
788
|
});
|