@servicenow/sdk-build-core 4.2.0 → 4.4.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 +14 -2
- package/dist/compiler.js +120 -8
- package/dist/compiler.js.map +1 -1
- package/dist/compression.d.ts +2 -2
- package/dist/compression.js +4 -1
- package/dist/compression.js.map +1 -1
- package/dist/now-config-dependencies.d.ts +2 -1
- package/dist/now-config.d.ts +10 -5
- package/dist/now-config.js +6 -5
- 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 +40 -5
- package/package.json +13 -9
- package/src/compiler.ts +140 -7
- package/src/compression.ts +2 -2
- package/src/now-config-dependencies.ts +2 -1
- package/src/now-config.ts +8 -6
- 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
|
@@ -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) {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Context, Database } from '../plugins'
|
|
2
|
+
|
|
3
|
+
export async function deleteMultipleDiff(
|
|
4
|
+
existing: Database,
|
|
5
|
+
incoming: Database,
|
|
6
|
+
descendants: Database,
|
|
7
|
+
{ factory }: Pick<Context, 'factory'>
|
|
8
|
+
) {
|
|
9
|
+
if (incoming.query().length === 0) {
|
|
10
|
+
// No changes for this record so we don't search descendents for absent deletes.
|
|
11
|
+
return {
|
|
12
|
+
success: true,
|
|
13
|
+
value: new Database(),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const changeDatabase = existing.compare(incoming)
|
|
18
|
+
const markForRemoval = descendants.query().filter((descendent) => !incoming.resolve(descendent.getId()))
|
|
19
|
+
for (const record of markForRemoval) {
|
|
20
|
+
const deleteRecord = await factory.createRecord({
|
|
21
|
+
source: record.getSource(),
|
|
22
|
+
table: record.getTable(),
|
|
23
|
+
explicitId: record.getId(),
|
|
24
|
+
properties: record.properties(),
|
|
25
|
+
action: 'DELETE',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
changeDatabase.insert(deleteRecord)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
value: changeDatabase.hasChanges() ? new Database(changeDatabase.query()) : new Database(),
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/util/index.ts
CHANGED