@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.
- package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Server/EnvironmentConfig.ts +1 -8
- 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/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -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/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -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/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +10 -46
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
- package/UI/Components/Charts/Line/LineChart.tsx +15 -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/EnvironmentConfig.js +1 -7
- 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/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/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- 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 +2 -33
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.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 +92 -38
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- 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,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
|
|
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
|
|
41
|
-
* sharing its email carried — the join the receiver makes
|
|
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() || "",
|
package/Server/Services/Index.ts
CHANGED
|
@@ -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
|
|
14
|
+
* form and the enterprise licence request form.
|
|
15
15
|
*
|
|
16
|
-
* Everything here is a whitelist, never a denylist.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
|
184
|
-
*
|
|
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
|
-
*
|
|
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`.
|
|
25
|
-
*
|
|
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
|
*
|