@microsoft/power-apps-native-common 0.1.8
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/LICENSE +112 -0
- package/README.md +15 -0
- package/lib/NOTICE.txt +179 -0
- package/lib/commonjs/TelemetryTypes.js +29 -0
- package/lib/commonjs/index.js +112 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/telemetry/AppLogger.js +49 -0
- package/lib/commonjs/telemetry/ConsoleAdapter.js +64 -0
- package/lib/commonjs/telemetry/ILogger.js +5 -0
- package/lib/commonjs/telemetry/Logger.js +151 -0
- package/lib/commonjs/telemetry/OneDsTelemetryProvider.js +276 -0
- package/lib/commonjs/telemetry/TelemetrySanitizer.js +100 -0
- package/lib/commonjs/telemetry/TelemetryTypes.js +29 -0
- package/lib/commonjs/telemetry/resolveTelemetryLocation.js +42 -0
- package/lib/commonjs/telemetry/stringifyError.js +31 -0
- package/lib/commonjs/telemetry/suppressKnownWarnings.js +28 -0
- package/lib/commonjs/types/ClusterCategory.js +54 -0
- package/lib/commonjs/utils/DeviceIdUtils.js +66 -0
- package/lib/commonjs/utils/GuidUtils.js +23 -0
- package/lib/commonjs/utils/PlatformInfoUtils.js +60 -0
- package/lib/module/TelemetryTypes.js +25 -0
- package/lib/module/index.js +18 -0
- package/lib/module/package.json +1 -0
- package/lib/module/telemetry/AppLogger.js +44 -0
- package/lib/module/telemetry/ConsoleAdapter.js +59 -0
- package/lib/module/telemetry/ILogger.js +3 -0
- package/lib/module/telemetry/Logger.js +146 -0
- package/lib/module/telemetry/OneDsTelemetryProvider.js +271 -0
- package/lib/module/telemetry/TelemetrySanitizer.js +94 -0
- package/lib/module/telemetry/TelemetryTypes.js +25 -0
- package/lib/module/telemetry/resolveTelemetryLocation.js +38 -0
- package/lib/module/telemetry/stringifyError.js +28 -0
- package/lib/module/telemetry/suppressKnownWarnings.js +25 -0
- package/lib/module/types/ClusterCategory.js +49 -0
- package/lib/module/utils/DeviceIdUtils.js +62 -0
- package/lib/module/utils/GuidUtils.js +19 -0
- package/lib/module/utils/PlatformInfoUtils.js +55 -0
- package/lib/typescript/commonjs/TelemetryTypes.d.ts +88 -0
- package/lib/typescript/commonjs/index.d.ts +19 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/telemetry/AppLogger.d.ts +35 -0
- package/lib/typescript/commonjs/telemetry/ConsoleAdapter.d.ts +24 -0
- package/lib/typescript/commonjs/telemetry/ILogger.d.ts +45 -0
- package/lib/typescript/commonjs/telemetry/Logger.d.ts +28 -0
- package/lib/typescript/commonjs/telemetry/OneDsTelemetryProvider.d.ts +39 -0
- package/lib/typescript/commonjs/telemetry/TelemetrySanitizer.d.ts +21 -0
- package/lib/typescript/commonjs/telemetry/TelemetryTypes.d.ts +88 -0
- package/lib/typescript/commonjs/telemetry/resolveTelemetryLocation.d.ts +26 -0
- package/lib/typescript/commonjs/telemetry/stringifyError.d.ts +10 -0
- package/lib/typescript/commonjs/telemetry/suppressKnownWarnings.d.ts +19 -0
- package/lib/typescript/commonjs/types/ClusterCategory.d.ts +37 -0
- package/lib/typescript/commonjs/utils/DeviceIdUtils.d.ts +29 -0
- package/lib/typescript/commonjs/utils/GuidUtils.d.ts +11 -0
- package/lib/typescript/commonjs/utils/PlatformInfoUtils.d.ts +33 -0
- package/lib/typescript/module/TelemetryTypes.d.ts +88 -0
- package/lib/typescript/module/index.d.ts +19 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/telemetry/AppLogger.d.ts +35 -0
- package/lib/typescript/module/telemetry/ConsoleAdapter.d.ts +24 -0
- package/lib/typescript/module/telemetry/ILogger.d.ts +45 -0
- package/lib/typescript/module/telemetry/Logger.d.ts +28 -0
- package/lib/typescript/module/telemetry/OneDsTelemetryProvider.d.ts +39 -0
- package/lib/typescript/module/telemetry/TelemetrySanitizer.d.ts +21 -0
- package/lib/typescript/module/telemetry/TelemetryTypes.d.ts +88 -0
- package/lib/typescript/module/telemetry/resolveTelemetryLocation.d.ts +26 -0
- package/lib/typescript/module/telemetry/stringifyError.d.ts +10 -0
- package/lib/typescript/module/telemetry/suppressKnownWarnings.d.ts +19 -0
- package/lib/typescript/module/types/ClusterCategory.d.ts +37 -0
- package/lib/typescript/module/utils/DeviceIdUtils.d.ts +29 -0
- package/lib/typescript/module/utils/GuidUtils.d.ts +11 -0
- package/lib/typescript/module/utils/PlatformInfoUtils.d.ts +33 -0
- package/package.json +83 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { AppInsightsCore } from '@microsoft/applicationinsights-core-js';
|
|
8
|
+
import { PostChannel } from '@microsoft/1ds-post-js';
|
|
9
|
+
import { generateGuid } from "../utils/GuidUtils.js";
|
|
10
|
+
import { resolvePlatformInfo } from "../utils/PlatformInfoUtils.js";
|
|
11
|
+
/** 1DS Aria project IDs shared by the webplayer and CLI. */
|
|
12
|
+
const AriaProjectIds = {
|
|
13
|
+
Production: '5c3a61f48d7c4522912765ae7465ab5c-fb3d6606-fe59-4d6b-9bb1-5e993d889672-7138',
|
|
14
|
+
NonProduction: '3fb21f3df5c44380bf452fd710f4d50c-0e3f82aa-6d27-41d8-b727-c96b28f99992-7504',
|
|
15
|
+
Mooncake: '1c1d139788f74386bd95e752dd781371-04b4424b-e5c1-46ae-8312-28e9fa5832c0-6923',
|
|
16
|
+
GccModerate: '0d7685a948a140789ac92933d3dc2d87-2dd0c1ce-8f10-41f0-a70a-2f9e51a851c8-6823',
|
|
17
|
+
GccHigh: 'd31229fb23b0406790b9332e5f465dfc-a6b19a0e-170d-4570-bda5-f35e3668e162-7067',
|
|
18
|
+
DoD: '41cc10dad35043adbcdace2896484ab4-5ebbc1d6-04c9-486d-a6b6-e5963cd778da-7542'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default property-to-column promotion map matching the webplayer convention.
|
|
23
|
+
* Properties matching these keys are promoted to top-level data columns;
|
|
24
|
+
* the rest go into the customDimensions JSON blob.
|
|
25
|
+
*/
|
|
26
|
+
const DefaultPropertyToColumn = {
|
|
27
|
+
apiId: 'apiId',
|
|
28
|
+
appSequenceNumber: 'appSequenceNumber',
|
|
29
|
+
duration: 'duration',
|
|
30
|
+
environmentId: 'environmentId',
|
|
31
|
+
httpMethod: 'httpMethod',
|
|
32
|
+
httpStatus: 'httpStatus',
|
|
33
|
+
operationName: 'operation_Name',
|
|
34
|
+
playerAppId: 'playerAppId',
|
|
35
|
+
scenarioGuid: 'scenarioGuid',
|
|
36
|
+
scenarioName: 'scenarioName',
|
|
37
|
+
scenarioResult: 'scenarioResult',
|
|
38
|
+
serverLocation: 'serverLocation',
|
|
39
|
+
url: 'url',
|
|
40
|
+
webPlayerUrl: 'webPlayerUrl'
|
|
41
|
+
};
|
|
42
|
+
function resolveInstrumentationKey(clusterCategory) {
|
|
43
|
+
switch (clusterCategory) {
|
|
44
|
+
case 'prod':
|
|
45
|
+
case 'firstrelease':
|
|
46
|
+
case 'ex':
|
|
47
|
+
case 'rx':
|
|
48
|
+
return AriaProjectIds.Production;
|
|
49
|
+
case 'mooncake':
|
|
50
|
+
return AriaProjectIds.Mooncake;
|
|
51
|
+
case 'gov':
|
|
52
|
+
return AriaProjectIds.GccModerate;
|
|
53
|
+
case 'high':
|
|
54
|
+
return AriaProjectIds.GccHigh;
|
|
55
|
+
case 'dod':
|
|
56
|
+
return AriaProjectIds.DoD;
|
|
57
|
+
default:
|
|
58
|
+
return AriaProjectIds.NonProduction;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function resolveCollectorUrl(clusterCategory, telemetryLocation = 'global') {
|
|
62
|
+
switch (clusterCategory) {
|
|
63
|
+
case 'mooncake':
|
|
64
|
+
return 'https://collector.azure.cn/OneCollector/1.0/';
|
|
65
|
+
case 'gov':
|
|
66
|
+
case 'high':
|
|
67
|
+
return 'https://tb.events.data.microsoft.com/OneCollector/1.0/';
|
|
68
|
+
case 'dod':
|
|
69
|
+
return 'https://pf.events.data.microsoft.com/OneCollector/1.0/';
|
|
70
|
+
case 'ex':
|
|
71
|
+
return 'https://collector.azure.eaglex.ic.gov/OneCollector/1.0/';
|
|
72
|
+
case 'rx':
|
|
73
|
+
return 'https://collector.azure.microsoft.scloud/OneCollector/1.0/';
|
|
74
|
+
default:
|
|
75
|
+
if (telemetryLocation === 'eu') {
|
|
76
|
+
return 'https://eu-mobile.events.data.microsoft.com/OneCollector/1.0/';
|
|
77
|
+
}
|
|
78
|
+
return 'https://us-mobile.events.data.microsoft.com/OneCollector/1.0/';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function resolveAppName(clusterCategory) {
|
|
82
|
+
switch (clusterCategory) {
|
|
83
|
+
case 'local':
|
|
84
|
+
case 'dev':
|
|
85
|
+
case 'test':
|
|
86
|
+
case 'preprod':
|
|
87
|
+
return 'powerappsclientnonprod';
|
|
88
|
+
default:
|
|
89
|
+
return 'powerappsclient';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export class OneDsTelemetryProvider {
|
|
93
|
+
defaultProperties = {};
|
|
94
|
+
constructor(config) {
|
|
95
|
+
this._sessionId = generateGuid();
|
|
96
|
+
this.appName = resolveAppName(config.clusterCategory);
|
|
97
|
+
this.appVersion = config.appVersion ?? '';
|
|
98
|
+
this.clientType = config.clientType;
|
|
99
|
+
this.clusterCategory = config.clusterCategory;
|
|
100
|
+
this.deviceId = config.deviceId ?? 'react-native';
|
|
101
|
+
this.minSeverity = config.minSeverity ?? 'info';
|
|
102
|
+
const {
|
|
103
|
+
osName,
|
|
104
|
+
osVersion,
|
|
105
|
+
deviceMake,
|
|
106
|
+
deviceModel,
|
|
107
|
+
deviceType,
|
|
108
|
+
userLocale,
|
|
109
|
+
userTimeZone
|
|
110
|
+
} = resolvePlatformInfo();
|
|
111
|
+
this.osName = osName;
|
|
112
|
+
this.osVersion = osVersion;
|
|
113
|
+
this.deviceMake = deviceMake;
|
|
114
|
+
this.deviceModel = deviceModel;
|
|
115
|
+
this.deviceType = deviceType;
|
|
116
|
+
this.userLocale = userLocale;
|
|
117
|
+
this.userTimeZone = userTimeZone;
|
|
118
|
+
|
|
119
|
+
// Merge default promotion map with consumer overrides (overrides win)
|
|
120
|
+
this.propertyColumnMap = {
|
|
121
|
+
...DefaultPropertyToColumn,
|
|
122
|
+
...config.propertyColumnMap
|
|
123
|
+
};
|
|
124
|
+
if (config.commonProperties) {
|
|
125
|
+
Object.assign(this.defaultProperties, config.commonProperties);
|
|
126
|
+
}
|
|
127
|
+
const postChannel = new PostChannel();
|
|
128
|
+
const instrumentationKey = config.instrumentationKey ?? resolveInstrumentationKey(config.clusterCategory);
|
|
129
|
+
const collectorUrl = resolveCollectorUrl(config.clusterCategory, config.telemetryLocation);
|
|
130
|
+
const coreConfig = {
|
|
131
|
+
instrumentationKey,
|
|
132
|
+
channels: [[postChannel]]
|
|
133
|
+
};
|
|
134
|
+
coreConfig.extensionConfig = {
|
|
135
|
+
[postChannel.identifier]: {
|
|
136
|
+
// Override endpoint explicitly — PostChannel reads overrideEndpointUrl
|
|
137
|
+
// from its own extensionConfig, not from coreConfig.endpointUrl.
|
|
138
|
+
overrideEndpointUrl: collectorUrl,
|
|
139
|
+
// Use React Native's fetch instead of PostChannel's XHR path, which
|
|
140
|
+
// uses browser-specific CORS and Beacon APIs not available in RN.
|
|
141
|
+
httpXHROverride: {
|
|
142
|
+
sendPOST: (payload, oncomplete) => {
|
|
143
|
+
const headers = {
|
|
144
|
+
...(payload.headers ?? {})
|
|
145
|
+
};
|
|
146
|
+
fetch(payload.urlString, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers,
|
|
149
|
+
body: typeof payload.data === 'string' ? payload.data : payload.data
|
|
150
|
+
}).then(async r => {
|
|
151
|
+
oncomplete(r.status, {});
|
|
152
|
+
}).catch(err => {
|
|
153
|
+
console.warn('[OneDsTelemetryProvider] sendPOST FAILED', err);
|
|
154
|
+
oncomplete(0, {});
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
...config.channelConfig
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
this.core = new AppInsightsCore();
|
|
162
|
+
this.core.initialize(coreConfig, []);
|
|
163
|
+
this.core.addTelemetryInitializer(envelope => {
|
|
164
|
+
const ext = envelope.ext = envelope.ext ?? {};
|
|
165
|
+
const utc = ext['utc'] = ext['utc'] ?? {};
|
|
166
|
+
utc['eventFlags'] = (utc['eventFlags'] || 0) | 0x100000;
|
|
167
|
+
const app = ext['app'] = ext['app'] ?? {};
|
|
168
|
+
app['sesId'] = this._sessionId;
|
|
169
|
+
if (this.appVersion) app['ver'] = this.appVersion; // → AppInfo_Version
|
|
170
|
+
const os = ext['os'] = ext['os'] ?? {};
|
|
171
|
+
os['name'] = this.osName; // → DeviceInfo_OsName
|
|
172
|
+
os['ver'] = this.osVersion; // → DeviceInfo_OsVersion
|
|
173
|
+
// ext.device.make/model → DeviceInfo_Make / DeviceInfo_Model
|
|
174
|
+
// Note: localId is intentionally omitted — it requires a strict UUID format
|
|
175
|
+
// and our device_Id column in data carries the same info once resolved.
|
|
176
|
+
const device = ext['device'] = ext['device'] ?? {};
|
|
177
|
+
if (this.deviceMake) device['make'] = this.deviceMake;
|
|
178
|
+
if (this.deviceModel) device['model'] = this.deviceModel;
|
|
179
|
+
if (this.deviceType) device['deviceClass'] = this.deviceType;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Register a telemetry initializer that runs on every envelope before send.
|
|
185
|
+
* Use this to add Part A Common Schema extensions (ext.app, ext.web, ext.os, etc.)
|
|
186
|
+
* that require platform-specific APIs (e.g. browser info, OS info).
|
|
187
|
+
*/
|
|
188
|
+
addTelemetryInitializer(initializer) {
|
|
189
|
+
this.core.addTelemetryInitializer(initializer);
|
|
190
|
+
}
|
|
191
|
+
track(event) {
|
|
192
|
+
const allProperties = {
|
|
193
|
+
...this.defaultProperties,
|
|
194
|
+
...event.properties
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Build data object with fixed columns matching webplayer convention
|
|
198
|
+
const data = {
|
|
199
|
+
app_Name: this.appName,
|
|
200
|
+
clientType: this.clientType,
|
|
201
|
+
clusterCategory: this.clusterCategory,
|
|
202
|
+
device_Id: this.deviceId,
|
|
203
|
+
DeviceInfo_Make: this.deviceMake,
|
|
204
|
+
...(this.deviceModel ? {
|
|
205
|
+
DeviceInfo_Model: this.deviceModel
|
|
206
|
+
} : {}),
|
|
207
|
+
userLocale: this.userLocale || undefined,
|
|
208
|
+
userTimeZone: this.userTimeZone || undefined,
|
|
209
|
+
event_Name: event.eventName,
|
|
210
|
+
sequenceNumber: event.sequenceNumber,
|
|
211
|
+
session_Id: this._sessionId,
|
|
212
|
+
severity: event.severity,
|
|
213
|
+
timestamp: event.timestamp
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// User identity promoted to top-level columns directly
|
|
217
|
+
if (this.userIdentity) {
|
|
218
|
+
data.objectId = this.userIdentity.objectId;
|
|
219
|
+
data.tenantId = this.userIdentity.tenantId;
|
|
220
|
+
}
|
|
221
|
+
if (event.duration !== undefined) {
|
|
222
|
+
data.duration = event.duration;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Promote properties matching the column map to top-level data fields
|
|
226
|
+
// Properties are kept in allProperties so they also appear in customDimensions
|
|
227
|
+
for (const [propKey, columnName] of Object.entries(this.propertyColumnMap)) {
|
|
228
|
+
if (propKey in allProperties) {
|
|
229
|
+
data[columnName] = allProperties[propKey];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Serialize remaining properties as customDimensions
|
|
234
|
+
data.customDimensions = JSON.stringify(allProperties);
|
|
235
|
+
const item = {
|
|
236
|
+
// 'event' routes all events to the Kusto `event` table.
|
|
237
|
+
// The actual event name is stored in data.event_Name.
|
|
238
|
+
name: 'event',
|
|
239
|
+
time: event.timestamp,
|
|
240
|
+
baseType: 'custom',
|
|
241
|
+
data
|
|
242
|
+
};
|
|
243
|
+
this.core.track(item);
|
|
244
|
+
}
|
|
245
|
+
flush() {
|
|
246
|
+
return new Promise(resolve => {
|
|
247
|
+
this.core.flush(true, () => {
|
|
248
|
+
resolve();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
teardown() {
|
|
253
|
+
return new Promise(resolve => {
|
|
254
|
+
this.core.unload(true, () => {
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
get sessionId() {
|
|
260
|
+
return this._sessionId;
|
|
261
|
+
}
|
|
262
|
+
set sessionId(value) {
|
|
263
|
+
this._sessionId = value;
|
|
264
|
+
}
|
|
265
|
+
identifyUser(identity) {
|
|
266
|
+
this.userIdentity = identity;
|
|
267
|
+
}
|
|
268
|
+
setDefaultProperty(name, value) {
|
|
269
|
+
this.defaultProperties[name] = value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Utilities for stripping PII and credentials from strings and property bags
|
|
9
|
+
* before they enter telemetry. All functions are pure and synchronous — safe
|
|
10
|
+
* to call on the hot path.
|
|
11
|
+
*
|
|
12
|
+
* Patterns detected (aligned with the CLI's ScrubUtils / CredScan KB):
|
|
13
|
+
* - Email / UPN addresses → '<upn>'
|
|
14
|
+
* - Private IPv4 addresses → '<private-ip>'
|
|
15
|
+
* - URL query strings / fragments → stripped (may carry tokens/user context)
|
|
16
|
+
* - Google API keys (AIza…) → '<credential>'
|
|
17
|
+
* - SAS key sig= parameter → '<credential>'
|
|
18
|
+
* - 256-bit symmetric keys (b64) → '<credential>'
|
|
19
|
+
*
|
|
20
|
+
* Property-bag rules:
|
|
21
|
+
* - Keys matching a PII/credential denylist → value replaced with '<redacted>'
|
|
22
|
+
* - String values run through sanitizeErrorString
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Matches email addresses / UPNs (simplified RFC 5322). */
|
|
26
|
+
const EMAIL_RE = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Matches private / loopback IPv4 addresses (RFC 1918 + 127.0.0.0/8 +
|
|
30
|
+
* link-local 169.254.0.0/16).
|
|
31
|
+
*/
|
|
32
|
+
const PRIVATE_IPV4_RE = /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|169\.254\.\d{1,3}\.\d{1,3})(:\d+)?\b/g;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Google API key pattern. Aligned with CredScan KB:
|
|
36
|
+
* https://msazure.visualstudio.com/One/_git/SecEng-CredScan-Core
|
|
37
|
+
*/
|
|
38
|
+
const GOOGLE_API_KEY_RE = /AIza[a-z0-9_\-]{35}/gi;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* SAS key sig= parameter. Aligned with CredScan KB (modified for JS):
|
|
42
|
+
* matches `sig=` or `dsas_secret=` followed by ≥8 non-whitespace chars
|
|
43
|
+
* (excluding delimiters that would indicate a false positive).
|
|
44
|
+
*/
|
|
45
|
+
const SAS_KEY_SIG_RE = /(?:dsas_secret|sig)=[^#)\(<>\r\n\s'",\\_&]{8}[^#)\(<>\r\n\s'",\\_&]*/gi;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 256-bit symmetric key in base64 (43 base64url chars + `=` pad).
|
|
49
|
+
* Aligned with CredScan KB SymmKey256 pattern.
|
|
50
|
+
*/
|
|
51
|
+
const SYMM_KEY_256_RE = /[a-z0-9/+]{43}=/gi;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Property key patterns that must never appear as telemetry values.
|
|
55
|
+
* Checked case-insensitively against the full key name.
|
|
56
|
+
*/
|
|
57
|
+
const SENSITIVE_KEY_RE = /password|secret|token|credential|auth(?:orization)?|bearer|apikey|api[_-]key|upn|email|username|accesstoken|refreshtoken|idtoken/i;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Redacts PII and credentials from a string.
|
|
61
|
+
* Safe to use on error messages, URL paths, and arbitrary text.
|
|
62
|
+
*/
|
|
63
|
+
export function sanitizeErrorString(raw) {
|
|
64
|
+
return raw.replace(EMAIL_RE, '<upn>').replace(PRIVATE_IPV4_RE, (_, port) => port ? `<private-ip>${port}` : '<private-ip>').replace(GOOGLE_API_KEY_RE, '<credential>').replace(SAS_KEY_SIG_RE, '<credential>').replace(SYMM_KEY_256_RE, '<credential>');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Strips the query string and URL fragment from a URL string, then redacts any
|
|
69
|
+
* PII/credentials in the remaining path.
|
|
70
|
+
*/
|
|
71
|
+
export function sanitizeUrl(url) {
|
|
72
|
+
const withoutQuery = url.replace(/[?#].*/, '');
|
|
73
|
+
return sanitizeErrorString(withoutQuery);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns a copy of `props` with PII and credentials scrubbed:
|
|
78
|
+
* - Keys matching SENSITIVE_KEY_RE → value replaced with '<redacted>'
|
|
79
|
+
* - String values run through sanitizeErrorString
|
|
80
|
+
* - Non-string primitives passed through unchanged
|
|
81
|
+
*/
|
|
82
|
+
export function sanitizeProperties(props) {
|
|
83
|
+
const result = {};
|
|
84
|
+
for (const [key, value] of Object.entries(props)) {
|
|
85
|
+
if (SENSITIVE_KEY_RE.test(key)) {
|
|
86
|
+
result[key] = '<redacted>';
|
|
87
|
+
} else if (typeof value === 'string') {
|
|
88
|
+
result[key] = sanitizeErrorString(value);
|
|
89
|
+
} else {
|
|
90
|
+
result[key] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Canonical 1DS event names matching webplayer conventions. */
|
|
8
|
+
export const TelemetryEventNames = {
|
|
9
|
+
Activity: 'PowerAppsClient.Activity',
|
|
10
|
+
Error: 'PowerAppsClient.Error',
|
|
11
|
+
StartScenario: 'PowerAppsClient.StartScenario',
|
|
12
|
+
EndScenario: 'PowerAppsClient.EndScenario'
|
|
13
|
+
};
|
|
14
|
+
export const ScenarioResults = {
|
|
15
|
+
Success: 'Success',
|
|
16
|
+
Failed: 'Failed',
|
|
17
|
+
SuccessWithErrors: 'SuccessWithErrors'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Telemetry data residency location. EU routes to eu-mobile collector. */
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Structural type for the 1DS telemetry envelope passed to telemetry initializers.
|
|
24
|
+
* Decouples consumers from the internal `ITelemetryItem` type.
|
|
25
|
+
*/
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Power Platform BAP environment `location` field values that fall under the
|
|
9
|
+
* EU data boundary. Mirrors `EUGeoRegions` from the CLI's `Logger/Constants.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Source: `metadata.properties.location` in the BAP Get Environments API response.
|
|
12
|
+
*/
|
|
13
|
+
const EU_BAP_LOCATIONS = new Set(['europe', 'france', 'germany', 'italy', 'poland', 'norway', 'sweden', 'switzerland', 'unitedkingdom']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the 1DS telemetry collector location from a Power Platform BAP
|
|
17
|
+
* environment `location` string (`environment.properties.location`).
|
|
18
|
+
* Returns `'eu'` when the location maps to the EU data boundary, routing
|
|
19
|
+
* telemetry to `eu-mobile.events.data.microsoft.com` instead of the global
|
|
20
|
+
* US collector.
|
|
21
|
+
*
|
|
22
|
+
* Mirrors the CLI's detection in `CliLogger.ts`:
|
|
23
|
+
* ```
|
|
24
|
+
* if (metadata?.properties?.location && EUGeoRegions.has(metadata.properties.location)) {
|
|
25
|
+
* telemetryLocation = 'eu';
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* resolveTelemetryLocation('europe') // → 'eu'
|
|
31
|
+
* resolveTelemetryLocation('unitedkingdom') // → 'eu'
|
|
32
|
+
* resolveTelemetryLocation('italy') // → 'eu'
|
|
33
|
+
* resolveTelemetryLocation('unitedstates') // → 'global'
|
|
34
|
+
*/
|
|
35
|
+
export function resolveTelemetryLocation(input) {
|
|
36
|
+
if (!input) return 'global';
|
|
37
|
+
return EU_BAP_LOCATIONS.has(input.trim().toLowerCase()) ? 'eu' : 'global';
|
|
38
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sanitizeErrorString } from "./TelemetrySanitizer.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Converts an unknown thrown value to a string suitable for telemetry.
|
|
11
|
+
* PII (email addresses, private IPv4 addresses) is redacted before the
|
|
12
|
+
* string is returned.
|
|
13
|
+
*/
|
|
14
|
+
export function stringifyError(error) {
|
|
15
|
+
let raw;
|
|
16
|
+
if (error instanceof Error) {
|
|
17
|
+
raw = `${error.name}: ${error.message}${error.stack ? `\n${error.stack}` : ''}`;
|
|
18
|
+
} else if (typeof error === 'string') {
|
|
19
|
+
raw = error;
|
|
20
|
+
} else {
|
|
21
|
+
try {
|
|
22
|
+
raw = JSON.stringify(error);
|
|
23
|
+
} catch {
|
|
24
|
+
raw = String(error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return sanitizeErrorString(raw);
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { LogBox } from 'react-native';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Suppresses known noisy LogBox warnings that originate from third-party
|
|
11
|
+
* packages and are not actionable in a React Native context.
|
|
12
|
+
*
|
|
13
|
+
* Call once at app startup (e.g. in the root `_layout.tsx` or `App.tsx`)
|
|
14
|
+
* before rendering any UI.
|
|
15
|
+
*
|
|
16
|
+
* **Why these warnings appear:**
|
|
17
|
+
* - `"Deprecated API for given entry type."` — emitted by React Native's
|
|
18
|
+
* Performance API when a caller requests an entry type (e.g. `"navigation"`,
|
|
19
|
+
* `"resource"`) that only exists in the browser W3C spec. The
|
|
20
|
+
* `@microsoft/power-apps` package calls `getEntriesByType('navigation')` for
|
|
21
|
+
* app-load telemetry; on React Native the call returns `[]` harmlessly.
|
|
22
|
+
*/
|
|
23
|
+
export function suppressKnownWarnings() {
|
|
24
|
+
LogBox.ignoreLogs(['Deprecated API for given entry type.']);
|
|
25
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Identifies the Power Platform cluster environment category.
|
|
9
|
+
* Used to resolve telemetry endpoints, instrumentation keys, and collector URLs.
|
|
10
|
+
*
|
|
11
|
+
* This is the **normalised** internal form used by telemetry providers.
|
|
12
|
+
* The raw `region` string from power.config.json may differ — use
|
|
13
|
+
* `regionToClusterCategory()` to convert before passing here.
|
|
14
|
+
*
|
|
15
|
+
* Canonical power.config.json `region` values → ClusterCategory mapping:
|
|
16
|
+
* preview → prod
|
|
17
|
+
* gccmoderate → gov
|
|
18
|
+
* gcchigh → high
|
|
19
|
+
* (all others are identity mappings)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* All valid `region` strings accepted in power.config.json.
|
|
24
|
+
* Mirrors the `Region` type from `@microsoft/power-apps-common/services`.
|
|
25
|
+
*/
|
|
26
|
+
export const Regions = ['local', 'dev', 'test', 'preprod', 'preview', 'prod', 'firstrelease', 'gccmoderate', 'gcchigh', 'dod', 'mooncake', 'ex', 'rx'];
|
|
27
|
+
/**
|
|
28
|
+
* Normalise a power.config.json `region` string to the internal
|
|
29
|
+
* `ClusterCategory` used by telemetry providers.
|
|
30
|
+
*
|
|
31
|
+
* Mirrors the CLI's `regionToClusterCategory` in OneDSWriter.ts:
|
|
32
|
+
* preview → prod (preview ring uses production telemetry)
|
|
33
|
+
* gccmoderate → gov (GCC Moderate sovereign cloud)
|
|
34
|
+
* gcchigh → high (GCC High sovereign cloud)
|
|
35
|
+
* firstrelease → firstrelease (first-release prod ring)
|
|
36
|
+
* all others → identity
|
|
37
|
+
*/
|
|
38
|
+
export function regionToClusterCategory(region) {
|
|
39
|
+
switch (region) {
|
|
40
|
+
case 'preview':
|
|
41
|
+
return 'prod';
|
|
42
|
+
case 'gccmoderate':
|
|
43
|
+
return 'gov';
|
|
44
|
+
case 'gcchigh':
|
|
45
|
+
return 'high';
|
|
46
|
+
default:
|
|
47
|
+
return region;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Application from 'expo-application';
|
|
8
|
+
import * as SecureStore from 'expo-secure-store';
|
|
9
|
+
import { Platform } from 'react-native';
|
|
10
|
+
import { generateGuid } from "./GuidUtils.js";
|
|
11
|
+
|
|
12
|
+
// Fallback key used when the platform native ID is unavailable.
|
|
13
|
+
// Only alphanumeric, dots, hyphens, and underscores are valid for SecureStore.
|
|
14
|
+
const DEVICE_ID_FALLBACK_KEY = 'microsoft.power-apps-native-common.device_id';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns a stable device identifier, preferring platform-native IDs that
|
|
18
|
+
* survive app reinstalls:
|
|
19
|
+
*
|
|
20
|
+
* - **iOS**: `getIosIdForVendorAsync()` (IDFV) — stable while ≥1 app from the
|
|
21
|
+
* same vendor bundle prefix is installed. Resets only when ALL vendor apps
|
|
22
|
+
* are removed.
|
|
23
|
+
* - **Android**: `getAndroidId()` (SSAID) — scoped to the app signing key on
|
|
24
|
+
* Android 8+; survives reinstalls as long as the signing key is unchanged.
|
|
25
|
+
*
|
|
26
|
+
* If the platform API returns null (rare edge cases: first boot, some emulators)
|
|
27
|
+
* the function falls back to a UUID persisted in `expo-secure-store`, which on
|
|
28
|
+
* iOS survives reinstalls via the Keychain and on Android is per-install only.
|
|
29
|
+
*
|
|
30
|
+
* Pass the resolved value to `OneDsTelemetryProviderConfig.deviceId` at startup.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { resolveDeviceId, OneDsTelemetryProvider } from '@microsoft/power-apps-native-common';
|
|
35
|
+
*
|
|
36
|
+
* const deviceId = await resolveDeviceId();
|
|
37
|
+
* const telemetry = new OneDsTelemetryProvider({ deviceId, ... });
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export async function resolveDeviceId() {
|
|
41
|
+
if (Platform.OS === 'ios') {
|
|
42
|
+
const idfv = await Application.getIosIdForVendorAsync();
|
|
43
|
+
if (idfv) {
|
|
44
|
+
return idfv;
|
|
45
|
+
}
|
|
46
|
+
} else if (Platform.OS === 'android') {
|
|
47
|
+
const androidId = Application.getAndroidId();
|
|
48
|
+
if (androidId) {
|
|
49
|
+
return androidId;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fallback: stable UUID in SecureStore (iOS: survives reinstall via Keychain;
|
|
54
|
+
// Android: per-install only, but only reached when SSAID is unavailable).
|
|
55
|
+
const existing = await SecureStore.getItemAsync(DEVICE_ID_FALLBACK_KEY);
|
|
56
|
+
if (existing) {
|
|
57
|
+
return existing;
|
|
58
|
+
}
|
|
59
|
+
const id = generateGuid();
|
|
60
|
+
await SecureStore.setItemAsync(DEVICE_ID_FALLBACK_KEY, id);
|
|
61
|
+
return id;
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Must be imported before 'uuid' so that the crypto.getRandomValues polyfill is
|
|
8
|
+
// in place when uuid initialises on Hermes/JSC (React Native).
|
|
9
|
+
import 'react-native-get-random-values';
|
|
10
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generates a RFC-4122 v4 GUID/UUID string.
|
|
14
|
+
* Uses the `uuid` package (backed by `react-native-get-random-values` on RN)
|
|
15
|
+
* instead of `crypto.randomUUID()` which is unavailable in Hermes/JSC.
|
|
16
|
+
*/
|
|
17
|
+
export function generateGuid() {
|
|
18
|
+
return uuidv4();
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*!
|
|
4
|
+
* Copyright (C) Microsoft Corporation. All rights reserved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Device from 'expo-device';
|
|
8
|
+
import { Platform } from 'react-native';
|
|
9
|
+
// Maps expo-device DeviceType numeric enum to a readable string.
|
|
10
|
+
const DEVICE_TYPE_LABEL = {
|
|
11
|
+
0: 'Unknown',
|
|
12
|
+
1: 'Phone',
|
|
13
|
+
2: 'Tablet',
|
|
14
|
+
3: 'Desktop',
|
|
15
|
+
4: 'TV'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolves 1DS Part A Common Schema device/OS/locale values using `expo-device`
|
|
20
|
+
* and the Intl API. Provides more accurate data than raw Platform.constants:
|
|
21
|
+
*
|
|
22
|
+
* - Correctly returns `'iPadOS'` for iPad (not `'iOS'`).
|
|
23
|
+
* - Returns human-friendly `modelName` (e.g. `'iPhone 16 Pro'`, `'Pixel 9'`).
|
|
24
|
+
* - Returns proper `manufacturer` (e.g. `'Apple'`, `'Google'`, `'Samsung'`).
|
|
25
|
+
* - Includes form-factor `deviceType` (Phone / Tablet / Desktop / TV).
|
|
26
|
+
*
|
|
27
|
+
* Safe to call once at provider construction time — all values are stable for
|
|
28
|
+
* the lifetime of the process.
|
|
29
|
+
*/
|
|
30
|
+
export function resolvePlatformInfo() {
|
|
31
|
+
// expo-device returns 'iOS' or 'iPadOS' on Apple platforms.
|
|
32
|
+
// On Android, osName maps to Build.VERSION.BASE_OS which can be a build
|
|
33
|
+
// fingerprint on some OEMs, so we always use the literal 'Android' there.
|
|
34
|
+
const osName = Platform.OS === 'ios' ? Device.osName ?? 'iOS' : 'Android';
|
|
35
|
+
const osVersion = Device.osVersion ?? String(Platform.Version);
|
|
36
|
+
const deviceMake = Device.manufacturer ?? (Platform.OS === 'ios' ? 'Apple' : '');
|
|
37
|
+
const deviceModel = Device.modelName ?? '';
|
|
38
|
+
const deviceType = DEVICE_TYPE_LABEL[Device.deviceType ?? 0] ?? 'Unknown';
|
|
39
|
+
let userLocale = '';
|
|
40
|
+
let userTimeZone = '';
|
|
41
|
+
try {
|
|
42
|
+
const dtf = Intl.DateTimeFormat().resolvedOptions();
|
|
43
|
+
userLocale = dtf.locale;
|
|
44
|
+
userTimeZone = dtf.timeZone;
|
|
45
|
+
} catch {/* Intl unavailable — leave empty */}
|
|
46
|
+
return {
|
|
47
|
+
osName,
|
|
48
|
+
osVersion,
|
|
49
|
+
deviceMake,
|
|
50
|
+
deviceModel,
|
|
51
|
+
deviceType,
|
|
52
|
+
userLocale,
|
|
53
|
+
userTimeZone
|
|
54
|
+
};
|
|
55
|
+
}
|