@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,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 } } = {};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import EnterpriseLicense from "../../Models/DatabaseModels/EnterpriseLicense";
|
|
2
2
|
import EnterpriseLicenseInstance from "../../Models/DatabaseModels/EnterpriseLicenseInstance";
|
|
3
3
|
import BadDataException from "../../Types/Exception/BadDataException";
|
|
4
|
-
import { JSONObject } from "../../Types/JSON";
|
|
5
4
|
import PartialEntity from "../../Types/Database/PartialEntity";
|
|
6
5
|
import EnterpriseLicenseInstanceSummary from "../../Types/EnterpriseLicense/EnterpriseLicenseInstanceSummary";
|
|
7
6
|
import EnterpriseLicenseUsageUtil from "../../Utils/EnterpriseLicense/EnterpriseLicenseUsage";
|
|
@@ -26,6 +25,20 @@ import {
|
|
|
26
25
|
import BaseAPI from "./BaseAPI";
|
|
27
26
|
// import { Host } from "../EnvironmentConfig";
|
|
28
27
|
|
|
28
|
+
/*
|
|
29
|
+
* The license state every installation of a license key mirrors locally.
|
|
30
|
+
* Returned by both /validate and /report-user-count so that the daily report
|
|
31
|
+
* keeps a self-hosted installation current on its own.
|
|
32
|
+
*/
|
|
33
|
+
export interface LicenseTerms {
|
|
34
|
+
companyName: string;
|
|
35
|
+
// ISO 8601, or null for a license with no expiry set.
|
|
36
|
+
expiresAt: string | null;
|
|
37
|
+
licenseKey: string;
|
|
38
|
+
// Null means the license carries no seat limit.
|
|
39
|
+
userLimit: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
29
42
|
export interface LicenseInstanceUpsert {
|
|
30
43
|
licenseId: ObjectID;
|
|
31
44
|
instanceId: string;
|
|
@@ -137,24 +150,15 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
137
150
|
const instances: Array<EnterpriseLicenseInstance> =
|
|
138
151
|
await this.findLicenseInstances(license.id!);
|
|
139
152
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
licenseKey: license.licenseKey || "",
|
|
144
|
-
userLimit:
|
|
145
|
-
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const token: string = JSONWebToken.signJsonPayload(
|
|
149
|
-
payload,
|
|
150
|
-
Math.max(secondsUntilExpiry, 1),
|
|
151
|
-
);
|
|
153
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
154
|
+
|
|
155
|
+
const token: string | null = this.signLicenseToken(license);
|
|
152
156
|
|
|
153
157
|
return Response.sendJsonObjectResponse(req, res, {
|
|
154
|
-
companyName:
|
|
155
|
-
expiresAt:
|
|
156
|
-
licenseKey:
|
|
157
|
-
userLimit:
|
|
158
|
+
companyName: terms.companyName,
|
|
159
|
+
expiresAt: terms.expiresAt,
|
|
160
|
+
licenseKey: terms.licenseKey,
|
|
161
|
+
userLimit: terms.userLimit,
|
|
158
162
|
currentUserCount:
|
|
159
163
|
typeof license.currentUserCount === "number"
|
|
160
164
|
? license.currentUserCount
|
|
@@ -202,8 +206,19 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
202
206
|
query: {
|
|
203
207
|
licenseKey: licenseKey,
|
|
204
208
|
},
|
|
209
|
+
/*
|
|
210
|
+
* The full set of terms, not just the usage columns: this
|
|
211
|
+
* response is what a self-hosted installation mirrors into its
|
|
212
|
+
* own GlobalConfig every day, so anything it is missing here is
|
|
213
|
+
* a field the customer keeps the stale value of until somebody
|
|
214
|
+
* re-types the license key by hand. Same indexed lookup either
|
|
215
|
+
* way.
|
|
216
|
+
*/
|
|
205
217
|
select: {
|
|
206
218
|
_id: true,
|
|
219
|
+
companyName: true,
|
|
220
|
+
expiresAt: true,
|
|
221
|
+
licenseKey: true,
|
|
207
222
|
userLimit: true,
|
|
208
223
|
isEvaluationLicense: true,
|
|
209
224
|
},
|
|
@@ -282,13 +297,25 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
282
297
|
});
|
|
283
298
|
}
|
|
284
299
|
|
|
300
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
301
|
+
|
|
285
302
|
return Response.sendJsonObjectResponse(req, res, {
|
|
303
|
+
companyName: terms.companyName,
|
|
304
|
+
expiresAt: terms.expiresAt,
|
|
305
|
+
licenseKey: terms.licenseKey,
|
|
306
|
+
userLimit: terms.userLimit,
|
|
286
307
|
currentUserCount: currentUserCount,
|
|
287
308
|
userCountUpdatedAt: reportedAt.toISOString(),
|
|
288
|
-
userLimit:
|
|
289
|
-
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
290
309
|
isEvaluationLicense: Boolean(license.isEvaluationLicense),
|
|
291
310
|
instances: this.getInstanceSummaries(instances),
|
|
311
|
+
/*
|
|
312
|
+
* Null once the license has expired. Unlike /validate this route
|
|
313
|
+
* does not reject an expired license - the instance is still
|
|
314
|
+
* expected to report, and the expiry notification emails are built
|
|
315
|
+
* from what it reports - but it must not keep being handed a fresh
|
|
316
|
+
* token, and the expiresAt above is what tells it the truth.
|
|
317
|
+
*/
|
|
318
|
+
token: this.signLicenseToken(license),
|
|
292
319
|
});
|
|
293
320
|
} catch (err) {
|
|
294
321
|
next(err);
|
|
@@ -297,6 +324,54 @@ export default class EnterpriseLicenseAPI extends BaseAPI<
|
|
|
297
324
|
);
|
|
298
325
|
}
|
|
299
326
|
|
|
327
|
+
/*
|
|
328
|
+
* The license terms a self-hosted installation mirrors locally. Built in one
|
|
329
|
+
* place so /validate and /report-user-count cannot drift: the seat limit was
|
|
330
|
+
* missing from what an instance could refresh for exactly as long as the two
|
|
331
|
+
* responses were written out by hand, separately.
|
|
332
|
+
*/
|
|
333
|
+
private getLicenseTerms(license: EnterpriseLicense): LicenseTerms {
|
|
334
|
+
return {
|
|
335
|
+
companyName: license.companyName || "",
|
|
336
|
+
expiresAt: license.expiresAt ? license.expiresAt.toISOString() : null,
|
|
337
|
+
licenseKey: license.licenseKey || "",
|
|
338
|
+
userLimit:
|
|
339
|
+
typeof license.userLimit === "number" ? license.userLimit : null,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/*
|
|
344
|
+
* Signed with the license server's own secret, so it is opaque to the
|
|
345
|
+
* installation holding it - it is the proof that oneuptime.com recognized
|
|
346
|
+
* the key, and the instance only checks that it exists. An expired license
|
|
347
|
+
* gets none, which is why this returns null rather than a short-lived token.
|
|
348
|
+
*/
|
|
349
|
+
private signLicenseToken(license: EnterpriseLicense): string | null {
|
|
350
|
+
if (!license.expiresAt) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const secondsUntilExpiry: number = Math.floor(
|
|
355
|
+
(license.expiresAt.getTime() - Date.now()) / 1000,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
if (secondsUntilExpiry <= 0) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const terms: LicenseTerms = this.getLicenseTerms(license);
|
|
363
|
+
|
|
364
|
+
return JSONWebToken.signJsonPayload(
|
|
365
|
+
{
|
|
366
|
+
companyName: terms.companyName,
|
|
367
|
+
expiresAt: terms.expiresAt,
|
|
368
|
+
licenseKey: terms.licenseKey,
|
|
369
|
+
userLimit: terms.userLimit,
|
|
370
|
+
},
|
|
371
|
+
Math.max(secondsUntilExpiry, 1),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
300
375
|
private parseShortText(value: unknown): string {
|
|
301
376
|
if (typeof value !== "string") {
|
|
302
377
|
return "";
|
|
@@ -369,16 +369,26 @@ export const AnalyticsKey: string = process.env["ANALYTICS_KEY"] || "";
|
|
|
369
369
|
export const AnalyticsHost: string = process.env["ANALYTICS_HOST"] || "";
|
|
370
370
|
|
|
371
371
|
/*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
372
|
+
* Whether this deployment loads the Google Tag Manager container and pushes
|
|
373
|
+
* marketing conversions into it.
|
|
374
|
+
*
|
|
375
|
+
* Billing being enabled is necessary — a self-hosted install must never load
|
|
376
|
+
* googletagmanager.com — but it is not sufficient. CI runs the SaaS end-to-end
|
|
377
|
+
* suite with BILLING_ENABLED forced to true, and those scripted registrations
|
|
378
|
+
* fire a real `sign_up` into the production container: the key-events trigger
|
|
379
|
+
* matches on the event name alone, with no hostname condition, and GA4 data
|
|
380
|
+
* filters do not apply retroactively. This is the second condition, so an
|
|
381
|
+
* environment can keep billing on and still stay out of the container.
|
|
382
|
+
*
|
|
383
|
+
* Defaults to on, so the hosted product needs no deploy change.
|
|
375
384
|
*/
|
|
376
|
-
export const
|
|
385
|
+
export const GoogleTagManagerEnabled: boolean =
|
|
386
|
+
IsBillingEnabled && process.env["GOOGLE_TAG_MANAGER_ENABLED"] !== "false";
|
|
377
387
|
|
|
378
388
|
/*
|
|
379
389
|
* Outbound marketing conversion webhooks.
|
|
380
390
|
*
|
|
381
|
-
* OneUptime does not store conversions. Signups
|
|
391
|
+
* OneUptime does not store conversions. Signups and plan
|
|
382
392
|
* upgrades/downgrades are POSTed to this endpoint as they happen and kept
|
|
383
393
|
* nowhere afterwards, so an unset URL means those moments are simply not
|
|
384
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;
|