@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.
Files changed (72) hide show
  1. package/LICENSE +112 -0
  2. package/README.md +15 -0
  3. package/lib/NOTICE.txt +179 -0
  4. package/lib/commonjs/TelemetryTypes.js +29 -0
  5. package/lib/commonjs/index.js +112 -0
  6. package/lib/commonjs/package.json +1 -0
  7. package/lib/commonjs/telemetry/AppLogger.js +49 -0
  8. package/lib/commonjs/telemetry/ConsoleAdapter.js +64 -0
  9. package/lib/commonjs/telemetry/ILogger.js +5 -0
  10. package/lib/commonjs/telemetry/Logger.js +151 -0
  11. package/lib/commonjs/telemetry/OneDsTelemetryProvider.js +276 -0
  12. package/lib/commonjs/telemetry/TelemetrySanitizer.js +100 -0
  13. package/lib/commonjs/telemetry/TelemetryTypes.js +29 -0
  14. package/lib/commonjs/telemetry/resolveTelemetryLocation.js +42 -0
  15. package/lib/commonjs/telemetry/stringifyError.js +31 -0
  16. package/lib/commonjs/telemetry/suppressKnownWarnings.js +28 -0
  17. package/lib/commonjs/types/ClusterCategory.js +54 -0
  18. package/lib/commonjs/utils/DeviceIdUtils.js +66 -0
  19. package/lib/commonjs/utils/GuidUtils.js +23 -0
  20. package/lib/commonjs/utils/PlatformInfoUtils.js +60 -0
  21. package/lib/module/TelemetryTypes.js +25 -0
  22. package/lib/module/index.js +18 -0
  23. package/lib/module/package.json +1 -0
  24. package/lib/module/telemetry/AppLogger.js +44 -0
  25. package/lib/module/telemetry/ConsoleAdapter.js +59 -0
  26. package/lib/module/telemetry/ILogger.js +3 -0
  27. package/lib/module/telemetry/Logger.js +146 -0
  28. package/lib/module/telemetry/OneDsTelemetryProvider.js +271 -0
  29. package/lib/module/telemetry/TelemetrySanitizer.js +94 -0
  30. package/lib/module/telemetry/TelemetryTypes.js +25 -0
  31. package/lib/module/telemetry/resolveTelemetryLocation.js +38 -0
  32. package/lib/module/telemetry/stringifyError.js +28 -0
  33. package/lib/module/telemetry/suppressKnownWarnings.js +25 -0
  34. package/lib/module/types/ClusterCategory.js +49 -0
  35. package/lib/module/utils/DeviceIdUtils.js +62 -0
  36. package/lib/module/utils/GuidUtils.js +19 -0
  37. package/lib/module/utils/PlatformInfoUtils.js +55 -0
  38. package/lib/typescript/commonjs/TelemetryTypes.d.ts +88 -0
  39. package/lib/typescript/commonjs/index.d.ts +19 -0
  40. package/lib/typescript/commonjs/package.json +1 -0
  41. package/lib/typescript/commonjs/telemetry/AppLogger.d.ts +35 -0
  42. package/lib/typescript/commonjs/telemetry/ConsoleAdapter.d.ts +24 -0
  43. package/lib/typescript/commonjs/telemetry/ILogger.d.ts +45 -0
  44. package/lib/typescript/commonjs/telemetry/Logger.d.ts +28 -0
  45. package/lib/typescript/commonjs/telemetry/OneDsTelemetryProvider.d.ts +39 -0
  46. package/lib/typescript/commonjs/telemetry/TelemetrySanitizer.d.ts +21 -0
  47. package/lib/typescript/commonjs/telemetry/TelemetryTypes.d.ts +88 -0
  48. package/lib/typescript/commonjs/telemetry/resolveTelemetryLocation.d.ts +26 -0
  49. package/lib/typescript/commonjs/telemetry/stringifyError.d.ts +10 -0
  50. package/lib/typescript/commonjs/telemetry/suppressKnownWarnings.d.ts +19 -0
  51. package/lib/typescript/commonjs/types/ClusterCategory.d.ts +37 -0
  52. package/lib/typescript/commonjs/utils/DeviceIdUtils.d.ts +29 -0
  53. package/lib/typescript/commonjs/utils/GuidUtils.d.ts +11 -0
  54. package/lib/typescript/commonjs/utils/PlatformInfoUtils.d.ts +33 -0
  55. package/lib/typescript/module/TelemetryTypes.d.ts +88 -0
  56. package/lib/typescript/module/index.d.ts +19 -0
  57. package/lib/typescript/module/package.json +1 -0
  58. package/lib/typescript/module/telemetry/AppLogger.d.ts +35 -0
  59. package/lib/typescript/module/telemetry/ConsoleAdapter.d.ts +24 -0
  60. package/lib/typescript/module/telemetry/ILogger.d.ts +45 -0
  61. package/lib/typescript/module/telemetry/Logger.d.ts +28 -0
  62. package/lib/typescript/module/telemetry/OneDsTelemetryProvider.d.ts +39 -0
  63. package/lib/typescript/module/telemetry/TelemetrySanitizer.d.ts +21 -0
  64. package/lib/typescript/module/telemetry/TelemetryTypes.d.ts +88 -0
  65. package/lib/typescript/module/telemetry/resolveTelemetryLocation.d.ts +26 -0
  66. package/lib/typescript/module/telemetry/stringifyError.d.ts +10 -0
  67. package/lib/typescript/module/telemetry/suppressKnownWarnings.d.ts +19 -0
  68. package/lib/typescript/module/types/ClusterCategory.d.ts +37 -0
  69. package/lib/typescript/module/utils/DeviceIdUtils.d.ts +29 -0
  70. package/lib/typescript/module/utils/GuidUtils.d.ts +11 -0
  71. package/lib/typescript/module/utils/PlatformInfoUtils.d.ts +33 -0
  72. 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
+ }