@servicenow/sdk-build-core 4.2.0 → 4.3.0
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/dist/compiler.d.ts +5 -1
- package/dist/compiler.js +104 -7
- package/dist/compiler.js.map +1 -1
- package/dist/now-config-dependencies.d.ts +2 -1
- package/dist/now-config.d.ts +5 -0
- package/dist/now-config.js +2 -4
- package/dist/now-config.js.map +1 -1
- package/dist/plugins/cache.d.ts +8 -13
- package/dist/plugins/cache.js +2 -26
- package/dist/plugins/cache.js.map +1 -1
- package/dist/plugins/data-shape.d.ts +24 -33
- package/dist/plugins/data-shape.js +83 -73
- package/dist/plugins/data-shape.js.map +1 -1
- package/dist/plugins/plugin.d.ts +43 -1
- package/dist/plugins/plugin.js +49 -5
- package/dist/plugins/plugin.js.map +1 -1
- package/dist/plugins/shape.d.ts +19 -11
- package/dist/plugins/shape.js +61 -23
- package/dist/plugins/shape.js.map +1 -1
- package/dist/taxonomy.js +53 -1
- package/dist/taxonomy.js.map +1 -1
- package/dist/telemetry/clients/abstract-client.d.ts +25 -0
- package/dist/telemetry/clients/abstract-client.js +55 -0
- package/dist/telemetry/clients/abstract-client.js.map +1 -0
- package/dist/telemetry/clients/browser-client.d.ts +20 -0
- package/dist/telemetry/clients/browser-client.js +136 -0
- package/dist/telemetry/clients/browser-client.js.map +1 -0
- package/dist/telemetry/clients/node-client.d.ts +15 -0
- package/dist/telemetry/clients/node-client.js +159 -0
- package/dist/telemetry/clients/node-client.js.map +1 -0
- package/dist/telemetry/clients/noop-client.d.ts +10 -0
- package/dist/telemetry/clients/noop-client.js +18 -0
- package/dist/telemetry/clients/noop-client.js.map +1 -0
- package/dist/telemetry/clients/util.d.ts +11 -0
- package/dist/telemetry/clients/util.js +34 -0
- package/dist/telemetry/clients/util.js.map +1 -0
- package/dist/telemetry/config.d.ts +2 -0
- package/dist/telemetry/config.js +28 -0
- package/dist/telemetry/config.js.map +1 -0
- package/dist/telemetry/factory.d.ts +13 -0
- package/dist/telemetry/factory.js +29 -0
- package/dist/telemetry/factory.js.map +1 -0
- package/dist/telemetry/index.d.ts +2 -25
- package/dist/telemetry/index.js +3 -15
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/types.d.ts +55 -0
- package/dist/telemetry/types.js +12 -0
- package/dist/telemetry/types.js.map +1 -0
- package/dist/typescript.d.ts +10 -0
- package/dist/typescript.js +18 -0
- package/dist/typescript.js.map +1 -1
- package/dist/util/delete-multiple.d.ts +5 -0
- package/dist/util/delete-multiple.js +30 -0
- package/dist/util/delete-multiple.js.map +1 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/index.js.map +1 -1
- package/now.config.schema.json +38 -0
- package/package.json +13 -9
- package/src/compiler.ts +121 -7
- package/src/now-config-dependencies.ts +2 -1
- package/src/now-config.ts +3 -4
- package/src/plugins/cache.ts +5 -27
- package/src/plugins/data-shape.ts +95 -84
- package/src/plugins/plugin.ts +116 -9
- package/src/plugins/shape.ts +64 -30
- package/src/taxonomy.ts +53 -1
- package/src/telemetry/clients/abstract-client.ts +63 -0
- package/src/telemetry/clients/browser-client.ts +160 -0
- package/src/telemetry/clients/node-client.ts +151 -0
- package/src/telemetry/clients/noop-client.ts +15 -0
- package/src/telemetry/clients/util.ts +33 -0
- package/src/telemetry/config.ts +12 -0
- package/src/telemetry/factory.ts +34 -0
- package/src/telemetry/index.ts +2 -27
- package/src/telemetry/types.ts +61 -0
- package/src/typescript.ts +17 -0
- package/src/util/delete-multiple.ts +35 -0
- package/src/util/index.ts +1 -0
package/src/taxonomy.ts
CHANGED
|
@@ -13,6 +13,31 @@ const TableNames = {
|
|
|
13
13
|
SYSEVENT_EMAIL_ACTION: 'sysevent_email_action',
|
|
14
14
|
SYS_PD_PROCESS_DEFINITION: 'sys_pd_process_definition',
|
|
15
15
|
SYS_HUB_TRIGGER_TEMPLATE: 'sys_hub_trigger_template',
|
|
16
|
+
SYS_VARIABLE_VALUE: 'sys_variable_value',
|
|
17
|
+
SYS_ELEMENT_MAPPING: 'sys_element_mapping',
|
|
18
|
+
SYS_HUB_FLOW_INPUT: 'sys_hub_flow_input',
|
|
19
|
+
SYS_HUB_FLOW_OUTPUT: 'sys_hub_flow_output',
|
|
20
|
+
SYS_HUB_ACTION_INPUT_ACTION_INSTANCE: 'sys_hub_action_input_action_instance',
|
|
21
|
+
SYS_HUB_FLOW_VARIABLE: 'sys_hub_flow_variable',
|
|
22
|
+
SYS_HUB_ACTION_INSTANCE: 'sys_hub_action_instance',
|
|
23
|
+
SYS_HUB_SUB_FLOW_INSTANCE: 'sys_hub_sub_flow_instance',
|
|
24
|
+
SYS_HUB_TRIGGER_INSTANCE: 'sys_hub_trigger_instance',
|
|
25
|
+
SYS_HUB_FLOW_LOGIC: 'sys_hub_flow_logic',
|
|
26
|
+
SYS_HUB_PILL_COMPOUND: 'sys_hub_pill_compound',
|
|
27
|
+
SYS_FLOW_CAT_VARIABLE_MODEL: 'sys_flow_cat_variable_model',
|
|
28
|
+
SYS_HUB_TRIGGER_INSTANCE_V2: 'sys_hub_trigger_instance_v2',
|
|
29
|
+
SYS_HUB_ACTION_INSTANCE_V2: 'sys_hub_action_instance_v2',
|
|
30
|
+
SYS_HUB_FLOW_LOGIC_INSTANCE_V2: 'sys_hub_flow_logic_instance_v2',
|
|
31
|
+
SYS_HUB_SUB_FLOW_INSTANCE_V2: 'sys_hub_sub_flow_instance_v2',
|
|
32
|
+
SYS_FLOW_RECORD_TRIGGER: 'sys_flow_record_trigger',
|
|
33
|
+
SYS_TRIGGER_RUNNER_MAPPING: 'sys_trigger_runner_mapping',
|
|
34
|
+
SYS_FLOW_TRIGGER_PLAN: 'sys_flow_trigger_plan',
|
|
35
|
+
SYS_HUB_FLOW_SNAPSHOT: 'sys_hub_flow_snapshot',
|
|
36
|
+
SYS_FLOW_SUBFLOW_PLAN: 'sys_flow_subflow_plan',
|
|
37
|
+
SYS_FLOW_EMAIL_TRIGGER: 'sys_flow_email_trigger',
|
|
38
|
+
SYS_FLOW_KNOWLEDGE_TRIGGER: 'sys_flow_knowledge_trigger',
|
|
39
|
+
SYS_FLOW_RT_QUERY_TRIGGER: 'sys_flow_rt_query_trigger',
|
|
40
|
+
SYS_FLOW_TRIGGER: 'sys_flow_trigger',
|
|
16
41
|
|
|
17
42
|
// Client Development tables
|
|
18
43
|
DL_U_ASSIGNMENT: 'dl_u_assignment',
|
|
@@ -156,6 +181,7 @@ const TableNames = {
|
|
|
156
181
|
|
|
157
182
|
// String format: one or more path segments separated by '/' (e.g., 'server-development/business-rule' or 'integration/outbound/rest-api')
|
|
158
183
|
const taxonomyItem = z.string().regex(/^[a-z0-9_-]+(\/[a-z0-9_-]+)*$/)
|
|
184
|
+
const flowTaxonomyItem = taxonomyItem.default('automation/flow')
|
|
159
185
|
|
|
160
186
|
const taxonomyMapping = z
|
|
161
187
|
.object({
|
|
@@ -164,12 +190,38 @@ const taxonomyMapping = z
|
|
|
164
190
|
[TableNames.SYS_HUB_TRIGGER_DEFINITION]: taxonomyItem.default('automation/trigger'),
|
|
165
191
|
[TableNames.SYS_PD_ACTIVITY_DEFINITION]: taxonomyItem.default('automation/activity-definition'),
|
|
166
192
|
[TableNames.SYS_ALIAS]: taxonomyItem.default('automation/workflow-activity'),
|
|
167
|
-
[TableNames.SYS_HUB_FLOW]: taxonomyItem.default('automation/flow'),
|
|
168
193
|
[TableNames.SYS_DECISION]: taxonomyItem.default('automation/decision-table'),
|
|
169
194
|
[TableNames.SYSEVENT_EMAIL_TEMPLATE]: taxonomyItem.default('automation/email-template'),
|
|
170
195
|
[TableNames.SYSEVENT_EMAIL_ACTION]: taxonomyItem.default('automation/notification'),
|
|
171
196
|
[TableNames.SYS_PD_PROCESS_DEFINITION]: taxonomyItem.default('automation/playbook'),
|
|
172
197
|
[TableNames.SYS_HUB_TRIGGER_TEMPLATE]: taxonomyItem.default('automation/trigger'),
|
|
198
|
+
[TableNames.SYS_FLOW_EMAIL_TRIGGER]: taxonomyItem.default('automation/trigger'),
|
|
199
|
+
[TableNames.SYS_FLOW_TRIGGER_PLAN]: taxonomyItem.default('automation/trigger'),
|
|
200
|
+
[TableNames.SYS_FLOW_KNOWLEDGE_TRIGGER]: taxonomyItem.default('automation/trigger'),
|
|
201
|
+
[TableNames.SYS_FLOW_RT_QUERY_TRIGGER]: taxonomyItem.default('automation/trigger'),
|
|
202
|
+
[TableNames.SYS_FLOW_TRIGGER]: taxonomyItem.default('automation/trigger'),
|
|
203
|
+
[TableNames.SYS_HUB_FLOW]: flowTaxonomyItem,
|
|
204
|
+
[TableNames.SYS_HUB_ACTION_TYPE_DEFINITION]: flowTaxonomyItem,
|
|
205
|
+
[TableNames.SYS_ELEMENT_MAPPING]: flowTaxonomyItem,
|
|
206
|
+
[TableNames.SYS_HUB_FLOW_INPUT]: flowTaxonomyItem,
|
|
207
|
+
[TableNames.SYS_HUB_FLOW_OUTPUT]: flowTaxonomyItem,
|
|
208
|
+
[TableNames.SYS_HUB_ACTION_INPUT_ACTION_INSTANCE]: flowTaxonomyItem,
|
|
209
|
+
[TableNames.SYS_HUB_FLOW_VARIABLE]: flowTaxonomyItem,
|
|
210
|
+
[TableNames.SYS_HUB_ACTION_INSTANCE]: flowTaxonomyItem,
|
|
211
|
+
[TableNames.SYS_HUB_SUB_FLOW_INSTANCE]: flowTaxonomyItem,
|
|
212
|
+
[TableNames.SYS_HUB_FLOW_LOGIC]: flowTaxonomyItem,
|
|
213
|
+
[TableNames.SYS_HUB_PILL_COMPOUND]: flowTaxonomyItem,
|
|
214
|
+
[TableNames.SYS_FLOW_CAT_VARIABLE_MODEL]: flowTaxonomyItem,
|
|
215
|
+
[TableNames.SYS_HUB_TRIGGER_INSTANCE_V2]: flowTaxonomyItem,
|
|
216
|
+
[TableNames.SYS_HUB_TRIGGER_INSTANCE]: flowTaxonomyItem,
|
|
217
|
+
[TableNames.SYS_HUB_ACTION_INSTANCE_V2]: flowTaxonomyItem,
|
|
218
|
+
[TableNames.SYS_HUB_FLOW_LOGIC_INSTANCE_V2]: flowTaxonomyItem,
|
|
219
|
+
[TableNames.SYS_HUB_SUB_FLOW_INSTANCE_V2]: flowTaxonomyItem,
|
|
220
|
+
[TableNames.SYS_FLOW_RECORD_TRIGGER]: flowTaxonomyItem,
|
|
221
|
+
[TableNames.SYS_TRIGGER_RUNNER_MAPPING]: flowTaxonomyItem,
|
|
222
|
+
[TableNames.SYS_FLOW_TRIGGER_PLAN]: flowTaxonomyItem,
|
|
223
|
+
[TableNames.SYS_HUB_FLOW_SNAPSHOT]: flowTaxonomyItem,
|
|
224
|
+
[TableNames.SYS_FLOW_SUBFLOW_PLAN]: flowTaxonomyItem,
|
|
173
225
|
|
|
174
226
|
// Client Development tables
|
|
175
227
|
[TableNames.DL_U_ASSIGNMENT]: taxonomyItem.default('client-development/assignment-data-lookup'),
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Telemetry, TelemetryEvent, TelemetryEventData, TimerMetric, InstanceSettings, Session } from '../types'
|
|
2
|
+
import { AppSeeTimerMetric } from './util'
|
|
3
|
+
|
|
4
|
+
export abstract class AbstractAppSeeClient implements Telemetry {
|
|
5
|
+
protected readonly sdkVersion?: string | undefined
|
|
6
|
+
protected readonly clientName: 'cli' | 'ide' | string
|
|
7
|
+
protected readonly hostname?: string | undefined
|
|
8
|
+
protected session?: Session
|
|
9
|
+
protected startPromise?: Promise<void>
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
protected readonly config: InstanceSettings,
|
|
13
|
+
protected telemetryAttributes: {
|
|
14
|
+
sdkVersion?: string
|
|
15
|
+
clientName: 'cli' | 'ide' | string
|
|
16
|
+
hostname?: string
|
|
17
|
+
}
|
|
18
|
+
) {
|
|
19
|
+
this.sdkVersion = telemetryAttributes.sdkVersion
|
|
20
|
+
this.clientName = telemetryAttributes.clientName
|
|
21
|
+
this.hostname = telemetryAttributes.hostname
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
startTimerEvent(name: TelemetryEvent['name']): TimerMetric {
|
|
25
|
+
return new AppSeeTimerMetric(name)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public start(): Promise<void> {
|
|
29
|
+
if (!this.startPromise) {
|
|
30
|
+
this.startPromise = this.doStart()
|
|
31
|
+
}
|
|
32
|
+
return this.startPromise
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public sendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
|
|
36
|
+
//we fire and forget the telemetry events
|
|
37
|
+
this.start()
|
|
38
|
+
.then(() => this.doSendEvent(events))
|
|
39
|
+
.catch(() => {})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public error(event: string, error: unknown, data?: TelemetryEventData) {
|
|
43
|
+
this.sendEvent([
|
|
44
|
+
{
|
|
45
|
+
name: 'error',
|
|
46
|
+
data: {
|
|
47
|
+
event,
|
|
48
|
+
...(error instanceof Error
|
|
49
|
+
? {
|
|
50
|
+
name: error.name,
|
|
51
|
+
message: error.message,
|
|
52
|
+
stack: error.stack,
|
|
53
|
+
}
|
|
54
|
+
: { message: error?.toString() }),
|
|
55
|
+
...data,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected abstract doStart(): Promise<void>
|
|
62
|
+
protected abstract doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void
|
|
63
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { TelemetryEvent, TelemetryEventData, Datapoint, HeartbeatBody } from '../types'
|
|
2
|
+
import { NETWORK_TIMEOUT_MS, DatapointType, type InstanceSettings } from '../types'
|
|
3
|
+
import { AbstractAppSeeClient } from './abstract-client'
|
|
4
|
+
|
|
5
|
+
export class BrowserTelemetryClient extends AbstractAppSeeClient {
|
|
6
|
+
constructor(
|
|
7
|
+
config: InstanceSettings,
|
|
8
|
+
telemetryAttributes?: {
|
|
9
|
+
sdkVersion?: string
|
|
10
|
+
clientName?: 'ide' | string
|
|
11
|
+
hostname?: string
|
|
12
|
+
ideVersion?: string
|
|
13
|
+
}
|
|
14
|
+
) {
|
|
15
|
+
const { ideVersion, ...baseAttributes } = telemetryAttributes ?? {}
|
|
16
|
+
super(config, {
|
|
17
|
+
...baseAttributes,
|
|
18
|
+
clientName: telemetryAttributes?.clientName ?? 'ide',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
this.ideVersion = ideVersion
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private readonly ideVersion: string | undefined
|
|
25
|
+
private userId?: string
|
|
26
|
+
|
|
27
|
+
private async getUserHash() {
|
|
28
|
+
const userUniqueId = await this.getUserIdentifier()
|
|
29
|
+
const bytes = new TextEncoder().encode(userUniqueId)
|
|
30
|
+
const byteHash = await crypto.subtle.digest('SHA-256', bytes)
|
|
31
|
+
const hashArray = Array.from(new Uint8Array(byteHash))
|
|
32
|
+
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async getUserIdentifier(): Promise<string> {
|
|
36
|
+
if (typeof window === 'undefined') {
|
|
37
|
+
return 'unknown'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const storageKey = 'sdk_telemetry_user_id'
|
|
41
|
+
try {
|
|
42
|
+
let userId = localStorage.getItem(storageKey)
|
|
43
|
+
|
|
44
|
+
if (!userId) {
|
|
45
|
+
userId = `anon_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`
|
|
46
|
+
localStorage.setItem(storageKey, userId)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return userId
|
|
50
|
+
} catch {
|
|
51
|
+
return `temp_${Math.random().toString(36).substring(2, 15)}`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private generateHexString(): string {
|
|
56
|
+
return crypto.randomUUID()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override async doStart(): Promise<void> {
|
|
60
|
+
try {
|
|
61
|
+
if (this.session) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.userId = await this.getUserHash()
|
|
66
|
+
|
|
67
|
+
const tabId = this.generateHexString()
|
|
68
|
+
const body = {
|
|
69
|
+
RequestId: this.generateHexString(),
|
|
70
|
+
TabId: tabId,
|
|
71
|
+
SystemLocale: navigator.language ?? 'unknown',
|
|
72
|
+
AppUserId: this.userId,
|
|
73
|
+
ScreenHeight: typeof screen !== 'undefined' ? screen.height : 0,
|
|
74
|
+
ScreenWidth: typeof screen !== 'undefined' ? screen.width : 0,
|
|
75
|
+
ClientTime: new Date().toISOString(),
|
|
76
|
+
TrackingLevel: 'Full',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const response = await fetch(new URL('/web/config', this.config.BaseUrl), {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: this.getHeaders(),
|
|
82
|
+
body: JSON.stringify(body),
|
|
83
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const bodyResponse = await response.json()
|
|
87
|
+
if (response.ok) {
|
|
88
|
+
this.session = { ...bodyResponse, TabId: tabId }
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
|
|
91
|
+
}
|
|
92
|
+
} catch (_error) {}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
override doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
|
|
96
|
+
try {
|
|
97
|
+
if (!this.session) {
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const defaultValues = this.getDefaultDataValues()
|
|
102
|
+
const eventData = Array.isArray(events) ? events : [events]
|
|
103
|
+
const now = Date.now()
|
|
104
|
+
const hostname = this.hostname ? { hostname: this.hostname } : {}
|
|
105
|
+
|
|
106
|
+
const dataPoints = eventData.map<Datapoint>((d) => ({
|
|
107
|
+
t: DatapointType.Event,
|
|
108
|
+
d: now,
|
|
109
|
+
n: d.name,
|
|
110
|
+
p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
|
|
111
|
+
}))
|
|
112
|
+
|
|
113
|
+
const userProps = { hostname } //does CI make sense for browser environment ??
|
|
114
|
+
|
|
115
|
+
dataPoints.push({
|
|
116
|
+
t: DatapointType.User,
|
|
117
|
+
d: now,
|
|
118
|
+
n: this.userId!,
|
|
119
|
+
p: userProps,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const body: HeartbeatBody = {
|
|
123
|
+
SessionId: this.session.SessionId,
|
|
124
|
+
DataPoints: dataPoints,
|
|
125
|
+
TabId: this.session.TabId || '0',
|
|
126
|
+
ClientTime: new Date().toISOString(),
|
|
127
|
+
ConfigReceivedTime: new Date().toISOString(),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: this.getHeaders(),
|
|
133
|
+
body: JSON.stringify(body),
|
|
134
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
135
|
+
}).catch(() => {})
|
|
136
|
+
} catch (_error) {}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private getHeaders() {
|
|
140
|
+
return {
|
|
141
|
+
APIKey: this.config.APIKey,
|
|
142
|
+
APIAuth: this.config.APIAuth,
|
|
143
|
+
BrowserId: '0',
|
|
144
|
+
ClientId: this.session?.ClientId || '0',
|
|
145
|
+
Version: this.sdkVersion || 'unknown',
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private getDefaultDataValues() {
|
|
151
|
+
const defaultValues: TelemetryEventData = {
|
|
152
|
+
browser: navigator.userAgent || 'unknown',
|
|
153
|
+
version: this.sdkVersion || 'unknown',
|
|
154
|
+
clientName: this.clientName,
|
|
155
|
+
ideVersion: this.ideVersion || 'unknown',
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return defaultValues
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as os from 'os'
|
|
2
|
+
import ciInfo from 'ci-info'
|
|
3
|
+
import { NETWORK_TIMEOUT_MS, DatapointType } from '../types'
|
|
4
|
+
import type { TelemetryEvent, TelemetryEventData, Datapoint, HeartbeatBody, InstanceSettings } from '../types'
|
|
5
|
+
import { AbstractAppSeeClient } from './abstract-client'
|
|
6
|
+
|
|
7
|
+
function generateHexString() {
|
|
8
|
+
// biome-ignore lint/style/noRestrictedImports: <explanation>
|
|
9
|
+
const crypto = require('node:crypto')
|
|
10
|
+
return crypto.randomBytes(16).toString('hex')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function getUserHash() {
|
|
14
|
+
const user = `${os.userInfo().username}:${os.hostname()}`
|
|
15
|
+
if (!user) {
|
|
16
|
+
return 'unknown'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const bytes = new TextEncoder().encode(user)
|
|
20
|
+
const byteHash = await crypto.subtle.digest('SHA-256', bytes)
|
|
21
|
+
return Buffer.from(byteHash).toString('hex')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class NodeTelemetryClient extends AbstractAppSeeClient {
|
|
25
|
+
constructor(
|
|
26
|
+
config: InstanceSettings,
|
|
27
|
+
telemetryAttributes?: { sdkVersion?: string; clientName?: 'cli' | string; hostname?: string }
|
|
28
|
+
) {
|
|
29
|
+
super(config, {
|
|
30
|
+
...(telemetryAttributes ?? {}),
|
|
31
|
+
clientName: telemetryAttributes?.clientName ?? 'cli',
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private userId?: string
|
|
36
|
+
|
|
37
|
+
override async doStart(): Promise<void> {
|
|
38
|
+
try {
|
|
39
|
+
if (this.session) {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.userId = await getUserHash()
|
|
44
|
+
|
|
45
|
+
const tabId = generateHexString()
|
|
46
|
+
const body = {
|
|
47
|
+
RequestId: generateHexString(),
|
|
48
|
+
TabId: tabId,
|
|
49
|
+
SystemLocale: (process.env['LANG'] || 'unknown').split('.')[0],
|
|
50
|
+
AppUserId: this.userId,
|
|
51
|
+
ScreenHeight: 0,
|
|
52
|
+
ScreenWidth: 0,
|
|
53
|
+
ClientTime: new Date().toISOString(),
|
|
54
|
+
TrackingLevel: 'Full',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const response = await fetch(new URL('/web/config', this.config.BaseUrl), {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: this.getHeaders(),
|
|
60
|
+
body: JSON.stringify(body),
|
|
61
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const bodyResponse = await response.json()
|
|
65
|
+
if (response.ok) {
|
|
66
|
+
this.session = { ...bodyResponse, TabId: tabId }
|
|
67
|
+
} else {
|
|
68
|
+
throw new Error(`Failed to start session with status ${response.status}: ${bodyResponse.Error}`)
|
|
69
|
+
}
|
|
70
|
+
} catch (_error) {
|
|
71
|
+
//ignore
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
override doSendEvent(events: TelemetryEvent | TelemetryEvent[]): void {
|
|
76
|
+
try {
|
|
77
|
+
//Check if we have a session
|
|
78
|
+
if (!this.session) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const defaultValues = this.getDefaultDataValues()
|
|
83
|
+
const eventData = Array.isArray(events) ? events : [events]
|
|
84
|
+
const now = Date.now()
|
|
85
|
+
const hostname = this.hostname ? { hostname: this.hostname } : {}
|
|
86
|
+
|
|
87
|
+
const dataPoints = eventData.map<Datapoint>((d) => ({
|
|
88
|
+
t: DatapointType.Event,
|
|
89
|
+
d: now,
|
|
90
|
+
n: d.name,
|
|
91
|
+
p: { scopeId: d.appInfo?.scopeId, ...hostname, ...defaultValues, ...d.data },
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
const userProps = this.applyCITelemetry({ ...hostname })
|
|
95
|
+
|
|
96
|
+
dataPoints.push({
|
|
97
|
+
t: DatapointType.User,
|
|
98
|
+
d: now,
|
|
99
|
+
n: this.userId!,
|
|
100
|
+
p: userProps,
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const body: HeartbeatBody = {
|
|
104
|
+
SessionId: this.session.SessionId,
|
|
105
|
+
DataPoints: dataPoints,
|
|
106
|
+
TabId: this.session.TabId || '0',
|
|
107
|
+
ClientTime: new Date().toISOString(),
|
|
108
|
+
ConfigReceivedTime: new Date().toISOString(),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
fetch(new URL('/web/heartbeat', this.config.BaseUrl), {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: this.getHeaders(),
|
|
114
|
+
body: JSON.stringify(body),
|
|
115
|
+
signal: AbortSignal.timeout(NETWORK_TIMEOUT_MS),
|
|
116
|
+
}).catch(() => {})
|
|
117
|
+
} catch (_error) {
|
|
118
|
+
//ignore errors from telemetry
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private getHeaders() {
|
|
123
|
+
return {
|
|
124
|
+
APIKey: this.config.APIKey,
|
|
125
|
+
APIAuth: this.config.APIAuth,
|
|
126
|
+
BrowserId: '0',
|
|
127
|
+
ClientId: this.session?.ClientId || '0',
|
|
128
|
+
Version: this.sdkVersion || 'unknown',
|
|
129
|
+
'Content-Type': 'application/json',
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private getDefaultDataValues() {
|
|
134
|
+
const defaultValues: TelemetryEventData = {
|
|
135
|
+
os: os.type(),
|
|
136
|
+
version: this.sdkVersion || 'unknown',
|
|
137
|
+
nodeVersion: process.version,
|
|
138
|
+
clientName: this.clientName,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.applyCITelemetry(defaultValues)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private applyCITelemetry(data: TelemetryEventData) {
|
|
145
|
+
if (ciInfo.isCI) {
|
|
146
|
+
return { ...data, ci: true, ciName: ciInfo.name, ciPR: ciInfo.isPR }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return data
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Telemetry, TimerMetric } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This is a no-op telemetry implementation that does nothing.
|
|
5
|
+
*/
|
|
6
|
+
export class NoOpTelemetry implements Telemetry {
|
|
7
|
+
startTimerEvent(): TimerMetric {
|
|
8
|
+
return {
|
|
9
|
+
end: () => ({ name: '', data: {} }),
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
start = () => {}
|
|
13
|
+
sendEvent = () => {}
|
|
14
|
+
error = () => {}
|
|
15
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { TelemetryEvent, TimerMetric } from '../types'
|
|
2
|
+
|
|
3
|
+
export class AppSeeTimerMetric implements TimerMetric {
|
|
4
|
+
private readonly startTime: number
|
|
5
|
+
|
|
6
|
+
constructor(private readonly name: TelemetryEvent['name']) {
|
|
7
|
+
this.startTime = Date.now()
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
getDurationMs() {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
return {
|
|
13
|
+
duration: now - this.startTime,
|
|
14
|
+
now,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
end(eventData?: Pick<TelemetryEvent, 'appInfo' | 'data'>): TelemetryEvent {
|
|
19
|
+
const durationMs = Date.now() - this.startTime
|
|
20
|
+
/**
|
|
21
|
+
* Create a an event containing the duration and all checkpoint durations
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
name: this.name,
|
|
26
|
+
appInfo: eventData?.appInfo || {},
|
|
27
|
+
data: {
|
|
28
|
+
...eventData?.data,
|
|
29
|
+
durationMs,
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { InstanceSettings } from './types'
|
|
2
|
+
|
|
3
|
+
declare const __APPSEE_API_KEY__: string
|
|
4
|
+
declare const __APPSEE_AUTH_KEY__: string
|
|
5
|
+
declare const __APPSEE_URL__: string
|
|
6
|
+
|
|
7
|
+
//will be populated by esbuild during the release build
|
|
8
|
+
export const getAppSeeConfig = (): InstanceSettings => ({
|
|
9
|
+
APIKey: __APPSEE_API_KEY__,
|
|
10
|
+
APIAuth: __APPSEE_AUTH_KEY__,
|
|
11
|
+
BaseUrl: __APPSEE_URL__,
|
|
12
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** biome-ignore-all lint/complexity/noStaticOnlyClass: <explanation> */
|
|
2
|
+
import type { Telemetry } from './types'
|
|
3
|
+
import { NoOpTelemetry } from './clients/noop-client'
|
|
4
|
+
import { NodeTelemetryClient } from './clients/node-client'
|
|
5
|
+
import { BrowserTelemetryClient } from './clients/browser-client'
|
|
6
|
+
import { getAppSeeConfig } from './config'
|
|
7
|
+
|
|
8
|
+
export class TelemetryFactory {
|
|
9
|
+
static create(options?: {
|
|
10
|
+
type?: 'node' | 'browser'
|
|
11
|
+
attributes?: { sdkVersion?: string; clientName?: string; hostname?: string; ideVersion?: string }
|
|
12
|
+
}): Telemetry {
|
|
13
|
+
const config = getAppSeeConfig()
|
|
14
|
+
|
|
15
|
+
if (
|
|
16
|
+
process.env['NODE_ENV'] === 'test' ||
|
|
17
|
+
!options ||
|
|
18
|
+
!options.type ||
|
|
19
|
+
!config.APIKey ||
|
|
20
|
+
!config.APIAuth ||
|
|
21
|
+
!config.BaseUrl
|
|
22
|
+
) {
|
|
23
|
+
return new NoOpTelemetry()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (options.type === 'node') {
|
|
27
|
+
return new NodeTelemetryClient(config, options.attributes)
|
|
28
|
+
} else if (options.type === 'browser') {
|
|
29
|
+
return new BrowserTelemetryClient(config, options.attributes)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error('Invalid telemetry type.')
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/telemetry/index.ts
CHANGED
|
@@ -1,27 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
error(event: string, error: unknown, data?: TelemetryEventData): Promise<void>
|
|
4
|
-
startTimerEvent(name: TelemetryEvent['name']): TimerMetric
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface TimerMetric {
|
|
8
|
-
end(eventData?: Pick<TelemetryEvent, 'appInfo' | 'data'>): TelemetryEvent
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type TelemetryEventData = Record<string, unknown>
|
|
12
|
-
|
|
13
|
-
export type TelemetryEvent = { name: string; appInfo?: { scopeId?: string }; data?: TelemetryEventData }
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* This is a no-op telemetry implementation that does nothing.
|
|
17
|
-
*/
|
|
18
|
-
export class NoOpTelemetry implements Telemetry {
|
|
19
|
-
startTimerEvent(): TimerMetric {
|
|
20
|
-
return {
|
|
21
|
-
end: () => ({ name: '', data: {} }),
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
start = () => Promise.resolve()
|
|
25
|
-
sendEvent = () => Promise.resolve()
|
|
26
|
-
error = () => Promise.resolve()
|
|
27
|
-
}
|
|
1
|
+
export type { Telemetry, TelemetryEvent } from './types'
|
|
2
|
+
export { TelemetryFactory } from './factory'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export const NETWORK_TIMEOUT_MS = 2000
|
|
2
|
+
|
|
3
|
+
export type InstanceSettings = {
|
|
4
|
+
APIKey: string
|
|
5
|
+
APIAuth: string
|
|
6
|
+
BaseUrl: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type Session = {
|
|
10
|
+
SessionId: string
|
|
11
|
+
ClientId: string
|
|
12
|
+
TabId: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export enum DatapointType {
|
|
16
|
+
Page = 0,
|
|
17
|
+
Event = 1,
|
|
18
|
+
User = 2,
|
|
19
|
+
PerformanceTrace = 3,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type Datapoint = {
|
|
23
|
+
t: DatapointType
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Epoch time in milliseconds
|
|
27
|
+
*/
|
|
28
|
+
d: number
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Event name
|
|
32
|
+
*/
|
|
33
|
+
n: string
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Event properties
|
|
37
|
+
*/
|
|
38
|
+
p?: Record<string, unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type HeartbeatBody = {
|
|
42
|
+
SessionId: string
|
|
43
|
+
DataPoints: Datapoint[]
|
|
44
|
+
TabId: string
|
|
45
|
+
ClientTime: string
|
|
46
|
+
ConfigReceivedTime: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface Telemetry {
|
|
50
|
+
sendEvent(eventData: TelemetryEvent | TelemetryEvent[]): void
|
|
51
|
+
error(event: string, error: unknown, data?: TelemetryEventData): void
|
|
52
|
+
startTimerEvent(name: TelemetryEvent['name']): TimerMetric
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TimerMetric {
|
|
56
|
+
end(eventData?: Pick<TelemetryEvent, 'appInfo' | 'data'>): TelemetryEvent
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type TelemetryEventData = Record<string, unknown>
|
|
60
|
+
|
|
61
|
+
export type TelemetryEvent = { name: string; appInfo?: { scopeId?: string }; data?: TelemetryEventData }
|
package/src/typescript.ts
CHANGED
|
@@ -31,6 +31,23 @@ import { ts as tsc } from 'ts-morph'
|
|
|
31
31
|
|
|
32
32
|
export { ts, tsc }
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Checks if a value is a valid ts-morph Node that can be operated on.
|
|
36
|
+
*
|
|
37
|
+
* Unlike `ts.Node.isNode()`, this function safely handles nodes that have been
|
|
38
|
+
* removed or forgotten from the AST, returning `false` instead of throwing.
|
|
39
|
+
*
|
|
40
|
+
* @param value - The value to check
|
|
41
|
+
* @returns `true` if the value is a valid, usable Node; `false` otherwise
|
|
42
|
+
*/
|
|
43
|
+
export function isValidNode(value: unknown): value is ts.Node {
|
|
44
|
+
try {
|
|
45
|
+
return ts.Node.isNode(value)
|
|
46
|
+
} catch {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
34
51
|
export function getValueDeclaration(node: ts.Node) {
|
|
35
52
|
const symbol = node.getSymbol()
|
|
36
53
|
if (!symbol) {
|