@oneuptime/common 12.0.20 → 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.
Files changed (86) hide show
  1. package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
  2. package/Models/AnalyticsModels/Index.ts +2 -0
  3. package/Server/EnvironmentConfig.ts +1 -8
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  6. package/Server/Services/ChangeEventService.ts +11 -0
  7. package/Server/Services/EnterpriseLicenseService.ts +3 -3
  8. package/Server/Services/Index.ts +2 -0
  9. package/Server/Utils/Attribution.ts +11 -11
  10. package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
  11. package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
  12. package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
  13. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
  14. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
  15. package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
  16. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
  17. package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
  18. package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
  19. package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
  20. package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
  21. package/Tests/Server/Utils/Attribution.test.ts +4 -5
  22. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
  23. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
  24. package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
  25. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
  26. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
  28. package/Types/Marketing/Attribution.ts +9 -15
  29. package/Types/Marketing/MarketingEvent.ts +10 -46
  30. package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
  31. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
  32. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
  33. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
  34. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
  35. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
  36. package/UI/Components/Charts/Line/LineChart.tsx +15 -0
  37. package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
  38. package/Utils/Telemetry/CrossSignalScope.ts +19 -10
  39. package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
  40. package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
  41. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  42. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  43. package/build/dist/Server/EnvironmentConfig.js +1 -7
  44. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  49. package/build/dist/Server/Services/ChangeEventService.js +9 -0
  50. package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
  51. package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
  52. package/build/dist/Server/Services/Index.js +2 -0
  53. package/build/dist/Server/Services/Index.js.map +1 -1
  54. package/build/dist/Server/Utils/Attribution.js +6 -6
  55. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
  56. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
  57. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
  58. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
  59. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
  60. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  61. package/build/dist/Types/Marketing/Attribution.js +9 -15
  62. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  63. package/build/dist/Types/Marketing/MarketingEvent.js +2 -33
  64. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  65. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  66. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  67. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
  68. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  69. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
  70. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  71. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
  72. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  73. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
  74. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  75. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
  76. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
  77. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  78. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  79. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
  80. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  81. package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
  82. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  83. package/package.json +1 -1
  84. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
  85. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
  86. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
@@ -0,0 +1,331 @@
1
+ import AnalyticsBaseModel from "./AnalyticsBaseModel/AnalyticsBaseModel";
2
+ import Route from "../../Types/API/Route";
3
+ import AnalyticsTableEngine from "../../Types/AnalyticsDatabase/AnalyticsTableEngine";
4
+ import AnalyticsTableName from "../../Types/AnalyticsDatabase/AnalyticsTableName";
5
+ import AnalyticsTableColumn, {
6
+ SkipIndexType,
7
+ } from "../../Types/AnalyticsDatabase/TableColumn";
8
+ import TableColumnType from "../../Types/AnalyticsDatabase/TableColumnType";
9
+ import OperationalResource from "../../Types/Database/AccessControl/OperationalResource";
10
+ import OwnedThrough from "../../Types/Database/AccessControl/OwnedThrough";
11
+ import { JSONObject } from "../../Types/JSON";
12
+ import ObjectID from "../../Types/ObjectID";
13
+ import Permission from "../../Types/Permission";
14
+ import Service from "../DatabaseModels/Service";
15
+ import ServiceType from "../../Types/Telemetry/ServiceType";
16
+
17
+ /*
18
+ * Deploys, config changes, scaling events — the "what changed?" signal.
19
+ * Rows are posted by CI/CD pipelines through the change-events ingest API
20
+ * (or created from the product) and rendered as vertical dashed markers
21
+ * on metric charts, so "the spike started 40 seconds after deploy X" is
22
+ * visible without leaving the chart.
23
+ *
24
+ * Peer of LogItemV3 in layout: tenant projectId, per-row retentionDate
25
+ * TTL, Map(String,String) attributes with a bloom-indexed keys sidecar.
26
+ * Deliberately narrow — this is an annotation stream, not a log store.
27
+ */
28
+
29
+ const readPermissions: Array<Permission> = [
30
+ Permission.ProjectOwner,
31
+ Permission.ProjectAdmin,
32
+ Permission.ProjectMember,
33
+ Permission.Viewer,
34
+ Permission.TelemetryAdmin,
35
+ Permission.TelemetryMember,
36
+ Permission.TelemetryViewer,
37
+ ];
38
+
39
+ const createPermissions: Array<Permission> = [
40
+ Permission.ProjectOwner,
41
+ Permission.ProjectAdmin,
42
+ Permission.ProjectMember,
43
+ Permission.TelemetryAdmin,
44
+ Permission.TelemetryMember,
45
+ ];
46
+
47
+ @OperationalResource()
48
+ @OwnedThrough("primaryEntityId", Service, { includeProjectScope: true })
49
+ export default class ChangeEvent extends AnalyticsBaseModel {
50
+ public constructor() {
51
+ const projectIdColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
52
+ key: "projectId",
53
+ title: "Project ID",
54
+ description: "ID of project",
55
+ required: true,
56
+ type: TableColumnType.ObjectID,
57
+ isTenantId: true,
58
+ accessControl: {
59
+ read: readPermissions,
60
+ create: createPermissions,
61
+ update: [],
62
+ },
63
+ });
64
+
65
+ const primaryEntityIdColumn: AnalyticsTableColumn =
66
+ new AnalyticsTableColumn({
67
+ key: "primaryEntityId",
68
+ title: "Service ID",
69
+ description:
70
+ "ID of the telemetry Service this change belongs to (the deployed service). Optional — project-wide changes carry none.",
71
+ required: false,
72
+ type: TableColumnType.ObjectID,
73
+ accessControl: {
74
+ read: readPermissions,
75
+ create: createPermissions,
76
+ update: [],
77
+ },
78
+ });
79
+
80
+ const primaryEntityTypeColumn: AnalyticsTableColumn =
81
+ new AnalyticsTableColumn({
82
+ key: "primaryEntityType",
83
+ isLowCardinality: true,
84
+ title: "Service Type",
85
+ description: "Discriminator for primaryEntityId",
86
+ required: false,
87
+ type: TableColumnType.Text,
88
+ accessControl: {
89
+ read: readPermissions,
90
+ create: createPermissions,
91
+ update: [],
92
+ },
93
+ });
94
+
95
+ const timeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
96
+ key: "time",
97
+ title: "Time",
98
+ description: "When the change happened",
99
+ required: true,
100
+ type: TableColumnType.DateTime64,
101
+ accessControl: {
102
+ read: readPermissions,
103
+ create: createPermissions,
104
+ update: [],
105
+ },
106
+ });
107
+
108
+ const eventTypeColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
109
+ key: "eventType",
110
+ isLowCardinality: true,
111
+ title: "Event Type",
112
+ description:
113
+ 'Kind of change — e.g. "deployment", "config-change", "scaling", "rollback", "custom"',
114
+ required: true,
115
+ defaultValue: "deployment",
116
+ type: TableColumnType.Text,
117
+ skipIndex: {
118
+ name: "idx_change_event_type",
119
+ type: SkipIndexType.Set,
120
+ params: [16],
121
+ granularity: 4,
122
+ },
123
+ accessControl: {
124
+ read: readPermissions,
125
+ create: createPermissions,
126
+ update: [],
127
+ },
128
+ });
129
+
130
+ const titleColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
131
+ key: "title",
132
+ title: "Title",
133
+ description: 'Short human label, e.g. "Deploy v2.31.0"',
134
+ required: true,
135
+ type: TableColumnType.Text,
136
+ accessControl: {
137
+ read: readPermissions,
138
+ create: createPermissions,
139
+ update: [],
140
+ },
141
+ });
142
+
143
+ const descriptionColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
144
+ key: "description",
145
+ title: "Description",
146
+ description: "Optional detail — commit message, change summary, links",
147
+ required: false,
148
+ type: TableColumnType.Text,
149
+ accessControl: {
150
+ read: readPermissions,
151
+ create: createPermissions,
152
+ update: [],
153
+ },
154
+ });
155
+
156
+ const attributesColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
157
+ key: "attributes",
158
+ codec: { codec: "ZSTD", level: 3 },
159
+ title: "Attributes",
160
+ description:
161
+ "Arbitrary tags — version, commit sha, pipeline url, environment",
162
+ required: true,
163
+ defaultValue: {},
164
+ type: TableColumnType.MapStringString,
165
+ mapKeysColumn: "attributeKeys",
166
+ accessControl: {
167
+ read: readPermissions,
168
+ create: createPermissions,
169
+ update: [],
170
+ },
171
+ });
172
+
173
+ const attributeKeysColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
174
+ key: "attributeKeys",
175
+ codec: { codec: "ZSTD", level: 3 },
176
+ title: "Attribute Keys",
177
+ description: "Attribute keys extracted from attributes",
178
+ required: true,
179
+ defaultValue: [],
180
+ type: TableColumnType.ArrayText,
181
+ skipIndex: {
182
+ name: "idx_attribute_keys",
183
+ type: SkipIndexType.BloomFilter,
184
+ params: [0.01],
185
+ granularity: 1,
186
+ },
187
+ accessControl: {
188
+ read: readPermissions,
189
+ create: createPermissions,
190
+ update: [],
191
+ },
192
+ });
193
+
194
+ const retentionDateColumn: AnalyticsTableColumn = new AnalyticsTableColumn({
195
+ key: "retentionDate",
196
+ codec: [{ codec: "DoubleDelta" }, { codec: "ZSTD", level: 1 }],
197
+ title: "Retention Date",
198
+ description:
199
+ "Date after which this row is eligible for TTL deletion, computed at ingest time",
200
+ required: true,
201
+ type: TableColumnType.Date,
202
+ defaultValue: undefined,
203
+ });
204
+
205
+ super({
206
+ tableName: AnalyticsTableName.ChangeEvent,
207
+ tableEngine: AnalyticsTableEngine.MergeTree,
208
+ singularName: "Change Event",
209
+ accessControl: {
210
+ read: readPermissions,
211
+ create: createPermissions,
212
+ update: [],
213
+ delete: [
214
+ Permission.ProjectOwner,
215
+ Permission.ProjectAdmin,
216
+ Permission.TelemetryAdmin,
217
+ ],
218
+ },
219
+ pluralName: "Change Events",
220
+ crudApiPath: new Route("/change-events"),
221
+ enableMCP: true,
222
+ enableDocumentation: true,
223
+ tableDescription:
224
+ "Change events (deployments, config changes, scaling) posted by CI/CD pipelines. Rendered as markers on metric charts so spikes can be correlated with what changed.",
225
+ tableColumns: [
226
+ projectIdColumn,
227
+ primaryEntityIdColumn,
228
+ primaryEntityTypeColumn,
229
+ timeColumn,
230
+ eventTypeColumn,
231
+ titleColumn,
232
+ descriptionColumn,
233
+ attributesColumn,
234
+ attributeKeysColumn,
235
+ retentionDateColumn,
236
+ ],
237
+ sortKeys: ["projectId", "time"],
238
+ primaryKeys: ["projectId", "time"],
239
+ partitionKey: "toYYYYMMDD(time)",
240
+ /*
241
+ * Tiny volume (a handful of rows per deploy) — shard by the
242
+ * always-present tenant + time pair; no hotspot risk at this rate.
243
+ */
244
+ shardingKey: "cityHash64(projectId, time)",
245
+ tableSettings:
246
+ "ttl_only_drop_parts = 1, non_replicated_deduplication_window = 10000",
247
+ ttlExpression: "retentionDate DELETE",
248
+ defaultSortColumn: "time",
249
+ });
250
+ }
251
+
252
+ public get projectId(): ObjectID | undefined {
253
+ return this.getColumnValue("projectId") as ObjectID | undefined;
254
+ }
255
+
256
+ public set projectId(v: ObjectID | undefined) {
257
+ this.setColumnValue("projectId", v);
258
+ }
259
+
260
+ public get primaryEntityId(): ObjectID | undefined {
261
+ return this.getColumnValue("primaryEntityId") as ObjectID | undefined;
262
+ }
263
+
264
+ public set primaryEntityId(v: ObjectID | undefined) {
265
+ this.setColumnValue("primaryEntityId", v);
266
+ }
267
+
268
+ public get primaryEntityType(): ServiceType | undefined {
269
+ return this.getColumnValue("primaryEntityType") as ServiceType | undefined;
270
+ }
271
+
272
+ public set primaryEntityType(v: ServiceType | undefined) {
273
+ this.setColumnValue("primaryEntityType", v);
274
+ }
275
+
276
+ public get time(): Date | undefined {
277
+ return this.getColumnValue("time") as Date | undefined;
278
+ }
279
+
280
+ public set time(v: Date | undefined) {
281
+ this.setColumnValue("time", v);
282
+ }
283
+
284
+ public get eventType(): string | undefined {
285
+ return this.getColumnValue("eventType") as string | undefined;
286
+ }
287
+
288
+ public set eventType(v: string | undefined) {
289
+ this.setColumnValue("eventType", v);
290
+ }
291
+
292
+ public get title(): string | undefined {
293
+ return this.getColumnValue("title") as string | undefined;
294
+ }
295
+
296
+ public set title(v: string | undefined) {
297
+ this.setColumnValue("title", v);
298
+ }
299
+
300
+ public get description(): string | undefined {
301
+ return this.getColumnValue("description") as string | undefined;
302
+ }
303
+
304
+ public set description(v: string | undefined) {
305
+ this.setColumnValue("description", v);
306
+ }
307
+
308
+ public get attributes(): JSONObject | undefined {
309
+ return this.getColumnValue("attributes") as JSONObject | undefined;
310
+ }
311
+
312
+ public set attributes(v: JSONObject | undefined) {
313
+ this.setColumnValue("attributes", v);
314
+ }
315
+
316
+ public get attributeKeys(): Array<string> | undefined {
317
+ return this.getColumnValue("attributeKeys") as Array<string> | undefined;
318
+ }
319
+
320
+ public set attributeKeys(v: Array<string> | undefined) {
321
+ this.setColumnValue("attributeKeys", v);
322
+ }
323
+
324
+ public get retentionDate(): Date | undefined {
325
+ return this.getColumnValue("retentionDate") as Date | undefined;
326
+ }
327
+
328
+ public set retentionDate(v: Date | undefined) {
329
+ this.setColumnValue("retentionDate", v);
330
+ }
331
+ }
@@ -21,6 +21,7 @@ import ProfileSample from "./ProfileSample";
21
21
  import RumSession from "./RumSession";
22
22
  import RumSessionChunk from "./RumSessionChunk";
23
23
  import AuditLog from "./AuditLog";
24
+ import ChangeEvent from "./ChangeEvent";
24
25
 
25
26
  const AnalyticsModels: Array<{ new (): AnalyticsBaseModel }> = [
26
27
  Log,
@@ -69,6 +70,7 @@ const AnalyticsModels: Array<{ new (): AnalyticsBaseModel }> = [
69
70
  RumSessionChunk,
70
71
  AuditLog,
71
72
  SecurityEvent,
73
+ ChangeEvent,
72
74
  ];
73
75
 
74
76
  const modelTypeMap: { [key: string]: { new (): AnalyticsBaseModel } } = {};
@@ -385,17 +385,10 @@ export const AnalyticsHost: string = process.env["ANALYTICS_HOST"] || "";
385
385
  export const GoogleTagManagerEnabled: boolean =
386
386
  IsBillingEnabled && process.env["GOOGLE_TAG_MANAGER_ENABLED"] !== "false";
387
387
 
388
- /*
389
- * Shared secret Cal.com signs booking webhooks with (App/API/CalWebhook.ts).
390
- * Server-only — must never be added to FRONTEND_ENV_ALLOW_LIST. Empty means
391
- * the webhook endpoint is disabled and answers 503.
392
- */
393
- export const CalWebhookSecret: string = process.env["CAL_WEBHOOK_SECRET"] || "";
394
-
395
388
  /*
396
389
  * Outbound marketing conversion webhooks.
397
390
  *
398
- * OneUptime does not store conversions. Signups, booked meetings and plan
391
+ * OneUptime does not store conversions. Signups and plan
399
392
  * upgrades/downgrades are POSTed to this endpoint as they happen and kept
400
393
  * nowhere afterwards, so an unset URL means those moments are simply not
401
394
  * measured — nothing accumulates waiting for one to be configured.
@@ -14,6 +14,10 @@ export class AddUserSlackAndMicrosoftTeams1789000000000
14
14
  public name = "AddUserSlackAndMicrosoftTeams1789000000000";
15
15
 
16
16
  public async up(queryRunner: QueryRunner): Promise<void> {
17
+ if (await this.isAlreadyApplied(queryRunner)) {
18
+ return;
19
+ }
20
+
17
21
  await queryRunner.query(
18
22
  `CREATE TABLE "UserSlack" ("_id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "deletedAt" TIMESTAMP WITH TIME ZONE, "version" integer NOT NULL, "projectId" uuid NOT NULL, "slackUserId" character varying(100), "slackUserName" character varying(100), "userId" uuid, "createdByUserId" uuid, "deletedByUserId" uuid, "isVerified" boolean NOT NULL DEFAULT false, CONSTRAINT "PK_86de4cd76b41a08811fc403e23a" PRIMARY KEY ("_id"))`,
19
23
  );
@@ -106,6 +110,44 @@ export class AddUserSlackAndMicrosoftTeams1789000000000
106
110
  );
107
111
  }
108
112
 
113
+ /*
114
+ * Did everything in `up` already run, under this migration's OLD name?
115
+ *
116
+ * This shipped as AddUserSlackAndMicrosoftTeams1788800000000 and was
117
+ * renumbered to 1789000000000 to clear a timestamp collision with
118
+ * DropMarketingConversionAddEnterpriseLicenseEmail. TypeORM identifies a
119
+ * migration by class NAME rather than by what it does, so every database
120
+ * that had already recorded the old name — anything tracking master between
121
+ * the two commits, which is every test and staging cluster — sees the
122
+ * renamed class as pending and runs it a second time, where
123
+ * `CREATE TABLE "UserSlack"` fails on the table it created itself. The
124
+ * runner stops at the first failure, so that single error holds back this
125
+ * migration and every migration added after it, on every boot.
126
+ *
127
+ * The probe is the table because the earlier run was all-or-nothing:
128
+ * migrations run one transaction each (DataSourceOptions sets
129
+ * migrationsTransactionMode: "each") and Postgres DDL is transactional, so
130
+ * "UserSlack" existing means every statement in `up` already committed.
131
+ * Nothing is left to do but let TypeORM record the new name.
132
+ *
133
+ * The superseded row in "migrations" is deliberately left alone. Deleting it
134
+ * would make this migration pending again for anyone who rolls the image
135
+ * BACK to a build that still registers the old name — the same failure, in
136
+ * the other direction. An unrecognised name in that table is inert.
137
+ */
138
+ private async isAlreadyApplied(queryRunner: QueryRunner): Promise<boolean> {
139
+ const userSlackTableExists: Array<{ exists: boolean }> =
140
+ await queryRunner.query(`
141
+ SELECT EXISTS (
142
+ SELECT FROM information_schema.tables
143
+ WHERE table_schema = 'public'
144
+ AND table_name = 'UserSlack'
145
+ )
146
+ `);
147
+
148
+ return Boolean(userSlackTableExists[0]?.exists);
149
+ }
150
+
109
151
  public async down(queryRunner: QueryRunner): Promise<void> {
110
152
  await queryRunner.query(
111
153
  `ALTER TABLE "UserOnCallLogTimeline" DROP CONSTRAINT "FK_a7206c19df5cdfe02cf3215a64c"`,
@@ -542,7 +542,6 @@ import { RemoveLlmCostBudgetAlertColumns1788500000000 } from "./1788500000000-Re
542
542
  import { AddDetectionRuleIncidentColumns1788600000000 } from "./1788600000000-AddDetectionRuleIncidentColumns";
543
543
  import { RemoveMarketingConversionUploadState1788700000000 } from "./1788700000000-RemoveMarketingConversionUploadState";
544
544
  import { DropMarketingConversionAddEnterpriseLicenseEmail1788800000000 } from "./1788800000000-DropMarketingConversionAddEnterpriseLicenseEmail";
545
- import { RedactStoredMonitorIngestSecrets1788900000000 } from "./1788900000000-RedactStoredMonitorIngestSecrets";
546
545
  import { AddUserSlackAndMicrosoftTeams1789000000000 } from "./1789000000000-AddUserSlackAndMicrosoftTeams";
547
546
  import { MigrationName1787142779538 } from "./1787142779538-MigrationName";
548
547
  import { MigrationName1787156982416 } from "./1787156982416-MigrationName";
@@ -1094,6 +1093,5 @@ export default [
1094
1093
  AddDetectionRuleIncidentColumns1788600000000,
1095
1094
  RemoveMarketingConversionUploadState1788700000000,
1096
1095
  DropMarketingConversionAddEnterpriseLicenseEmail1788800000000,
1097
- RedactStoredMonitorIngestSecrets1788900000000,
1098
1096
  AddUserSlackAndMicrosoftTeams1789000000000,
1099
1097
  ];
@@ -0,0 +1,11 @@
1
+ import ClickhouseDatabase from "../Infrastructure/ClickhouseDatabase";
2
+ import AnalyticsDatabaseService from "./AnalyticsDatabaseService";
3
+ import ChangeEvent from "../../Models/AnalyticsModels/ChangeEvent";
4
+
5
+ export class ChangeEventService extends AnalyticsDatabaseService<ChangeEvent> {
6
+ public constructor(clickhouseDatabase?: ClickhouseDatabase | undefined) {
7
+ super({ modelType: ChangeEvent, database: clickhouseDatabase });
8
+ }
9
+ }
10
+
11
+ export default new ChangeEventService();
@@ -37,9 +37,9 @@ export class Service extends DatabaseService<EnterpriseLicense> {
37
37
  email: createdItem.email?.toString(),
38
38
  /*
39
39
  * No attribution source. A licence row is typed in by a human and
40
- * carries no session, so its campaign is whatever the meeting_booked
41
- * sharing its email carried — the join the receiver makes on
42
- * emailHash, not something OneUptime can restate here.
40
+ * carries no session, so its campaign is whatever the earlier
41
+ * conversion sharing its email carried — the join the receiver makes
42
+ * on emailHash, not something OneUptime can restate here.
43
43
  */
44
44
  data: {
45
45
  enterpriseLicenseId: createdItem.id?.toString() || "",
@@ -77,6 +77,7 @@ import DataSourceService from "./DataSourceService";
77
77
  import AuditLogService from "./AuditLogService";
78
78
  import LogService from "./LogService";
79
79
  import SecurityEventService from "./SecurityEventService";
80
+ import ChangeEventService from "./ChangeEventService";
80
81
  import MailService from "./MailService";
81
82
  import MetricService from "./MetricService";
82
83
  import MetricItemAggMV1mService from "./MetricItemAggMV1mService";
@@ -641,6 +642,7 @@ export const AnalyticsServices: Array<
641
642
  RumSessionChunkService,
642
643
  AuditLogService,
643
644
  SecurityEventService,
645
+ ChangeEventService,
644
646
  ];
645
647
 
646
648
  export default services;
@@ -11,12 +11,12 @@ import { JSONObject, JSONValue } from "../../Types/JSON";
11
11
 
12
12
  /*
13
13
  * Sanitizers for attribution submitted by unauthenticated callers — the signup
14
- * form, the Cal.com booking webhook, and the enterprise licence request form.
14
+ * form and the enterprise licence request form.
15
15
  *
16
- * Everything here is a whitelist, never a denylist. Cal booking metadata and
17
- * form bodies are free-form caller content (names, notes, answers to booking
18
- * questions), and only the keys named in Common/Types/Marketing/Attribution.ts
19
- * may be copied into a row that is later forwarded to an ad platform.
16
+ * Everything here is a whitelist, never a denylist. Form bodies are free-form
17
+ * caller content (names, notes, free-text answers), and only the keys named in
18
+ * Common/Types/Marketing/Attribution.ts may be copied into a row that is later
19
+ * forwarded to an ad platform.
20
20
  *
21
21
  * This file also owns email normalisation and hashing. Meta and Reddit each
22
22
  * used to carry a private copy of the hash, and MarketingConversion.emailHash
@@ -101,10 +101,10 @@ export default class Attribution {
101
101
  /*
102
102
  * UTM values out of an arbitrary object, accepting either spelling.
103
103
  *
104
- * A URL and Cal booking metadata carry `utm_source`; a JSON body posted by
105
- * the signup page carries `utmSource`. Both arrive at server doors that
106
- * write the same columns, so both are read here rather than making each
107
- * caller remember which shape it is holding. camelCase wins when a caller
104
+ * A URL carries `utm_source`; a JSON body posted by the signup page carries
105
+ * `utmSource`. Both arrive at server doors that write the same columns, so
106
+ * both are read here rather than making each caller remember which shape it
107
+ * is holding. camelCase wins when a caller
108
108
  * somehow sends both, because that is the spelling the browser writes
109
109
  * deliberately and the snake_case one is the raw URL echo.
110
110
  */
@@ -180,8 +180,8 @@ export default class Attribution {
180
180
 
181
181
  /*
182
182
  * SHA-256 of the normalised email, hex encoded — the identifier every ad
183
- * platform's enhanced matching expects, and the key the conversion ledger
184
- * joins a booked meeting to the signup it later produced.
183
+ * platform's enhanced matching expects, and the key a receiver joins one
184
+ * person's conversions on.
185
185
  */
186
186
  public static hashEmail(email: string | undefined): string | null {
187
187
  const normalized: string | null = this.normalizeEmail(email);
@@ -12,8 +12,8 @@ const REQUEST_TIMEOUT_MS: number = 15000;
12
12
  /*
13
13
  * Delivers marketing conversion events to one operator-configured endpoint.
14
14
  *
15
- * OneUptime keeps no conversion ledger, so this is the only exit for a signup,
16
- * a booked meeting or a plan change. That has one consequence worth stating
15
+ * OneUptime keeps no conversion ledger, so this is the only exit for a signup
16
+ * or a plan change. That has one consequence worth stating
17
17
  * plainly: a delivery this class gives up on is gone, because there is no row
18
18
  * anywhere to reconcile against later. deliver() therefore THROWS on anything
19
19
  * that might succeed on a retry, and the queue is what retries it.
@@ -21,9 +21,8 @@ const REQUEST_TIMEOUT_MS: number = 15000;
21
21
  * SIGNING
22
22
  *
23
23
  * The signature is HMAC-SHA256 over the exact request body bytes, hex encoded,
24
- * in `x-oneuptime-signature-256`. This is the same scheme OneUptime verifies
25
- * on the way in from Cal, deliberately: one thing to understand rather than
26
- * two. The receiver must compute its digest over the raw bytes it received —
24
+ * in `x-oneuptime-signature-256`. The receiver must compute its digest over
25
+ * the raw bytes it received
27
26
  * JSON parsed and re-serialised is NOT equivalent, because whitespace, key
28
27
  * order and escaping all change the digest.
29
28
  *