@absolutejs/absolute 0.20.0-beta.67 → 0.20.0-beta.69

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.
@@ -6710,6 +6710,141 @@ var installAbsoluteMobileShellObservability = (manifest, engine, fetch2 = global
6710
6710
  };
6711
6711
  };
6712
6712
 
6713
+ // src/mobile/shellNativeObservability.ts
6714
+ import { Capacitor, registerPlugin } from "@capacitor/core";
6715
+ var MAX_REPORTS = 8;
6716
+ var MAX_DETAIL_BYTES = 64 * 1024;
6717
+ var MAX_STRING_LENGTH = 8 * 1024;
6718
+ var MAX_REDACTION_DEPTH = 8;
6719
+ var REPORT_ID = /^[A-Za-z0-9._:-]{1,128}$/u;
6720
+ var REPORT_KINDS = new Set([
6721
+ "anr",
6722
+ "app-launch",
6723
+ "cpu-exception",
6724
+ "crash",
6725
+ "disk-write-exception",
6726
+ "hang",
6727
+ "initialization-failure",
6728
+ "low-memory",
6729
+ "native-crash",
6730
+ "resource-exhaustion",
6731
+ "signal"
6732
+ ]);
6733
+ var nativeObservability = registerPlugin("AbsoluteMobileObservability");
6734
+ var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
6735
+ var redactText = (value) => value.slice(0, MAX_STRING_LENGTH).replace(/\b(Bearer)\s+[A-Za-z0-9._~+/-]+=*/giu, "$1 [REDACTED]").replace(/([?&](?:access_token|api_?key|authorization|code|id_token|password|refresh_token|secret|token)=)[^&#\s]*/giu, "$1[REDACTED]").replace(/\b((?:access_?token|api_?key|authorization|client_?secret|cookie|id_?token|password|passwd|refresh_?token|secret|token)\s*[:=]\s*)[^,\s;]+/giu, "$1[REDACTED]").replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/gu, "[REDACTED]");
6736
+ var redactValue2 = (value, depth = 0) => {
6737
+ if (depth >= MAX_REDACTION_DEPTH)
6738
+ return "[TRUNCATED]";
6739
+ if (typeof value === "string")
6740
+ return redactText(value);
6741
+ if (value === null || typeof value === "number" || typeof value === "boolean")
6742
+ return value;
6743
+ if (Array.isArray(value))
6744
+ return value.slice(0, 64).map((entry) => redactValue2(entry, depth + 1));
6745
+ if (!isRecord3(value))
6746
+ return String(value).slice(0, MAX_STRING_LENGTH);
6747
+ return Object.fromEntries(Object.entries(value).slice(0, 128).map(([key, entry]) => [
6748
+ key,
6749
+ /(?:authorization|cookie|password|secret|token)/iu.test(key) ? "[REDACTED]" : redactValue2(entry, depth + 1)
6750
+ ]));
6751
+ };
6752
+ var parseReport = (value) => {
6753
+ if (!isRecord3(value) || !isRecord3(value.details))
6754
+ return;
6755
+ if (typeof value.id !== "string" || !REPORT_ID.test(value.id))
6756
+ return;
6757
+ if (typeof value.kind !== "string" || !REPORT_KINDS.has(value.kind))
6758
+ return;
6759
+ if (typeof value.occurredAt !== "number" || !Number.isFinite(value.occurredAt) || value.occurredAt <= 0)
6760
+ return;
6761
+ if (value.platform !== "android" && value.platform !== "ios")
6762
+ return;
6763
+ const details = redactValue2(value.details);
6764
+ if (!isRecord3(details))
6765
+ return;
6766
+ if (new TextEncoder().encode(JSON.stringify(details)).byteLength > MAX_DETAIL_BYTES)
6767
+ return;
6768
+ return {
6769
+ details,
6770
+ id: value.id,
6771
+ kind: value.kind,
6772
+ occurredAt: value.occurredAt,
6773
+ platform: value.platform
6774
+ };
6775
+ };
6776
+ var sampled = (id, sampleRate) => {
6777
+ if (sampleRate >= 1)
6778
+ return true;
6779
+ let hash = 2166136261;
6780
+ for (const character of id) {
6781
+ hash ^= character.codePointAt(0) ?? 0;
6782
+ hash = Math.imul(hash, 16777619);
6783
+ }
6784
+ return (hash >>> 0) / 4294967296 < sampleRate;
6785
+ };
6786
+ var reportEvent = (report, manifest) => ({
6787
+ at: report.occurredAt,
6788
+ extra: {
6789
+ nativeDiagnostic: report.details,
6790
+ nativeDiagnosticId: report.id
6791
+ },
6792
+ groupingKey: `absolute-native:${report.platform}:${report.kind}`,
6793
+ level: [
6794
+ "app-launch",
6795
+ "cpu-exception",
6796
+ "disk-write-exception",
6797
+ "low-memory",
6798
+ "resource-exhaustion",
6799
+ "signal"
6800
+ ].includes(report.kind) ? "warning" : "error",
6801
+ message: `Native process diagnostic — ${report.kind}`,
6802
+ name: "AbsoluteMobileNativeDiagnostic",
6803
+ tags: {
6804
+ absoluteMobile: "true",
6805
+ mobileAppBuild: manifest.appBuild,
6806
+ mobileFailurePhase: "native-process",
6807
+ mobileManifestFormat: String(manifest.format),
6808
+ mobileNativeRuntime: manifest.nativeRuntime,
6809
+ mobilePlatform: report.platform,
6810
+ mobileRuntime: manifest.runtime
6811
+ }
6812
+ });
6813
+ var drainAbsoluteMobileNativeObservability = async (manifest, fetch2, options = {}) => {
6814
+ const config = manifest.observability;
6815
+ if (!config || !(options.native ?? Capacitor.isNativePlatform()))
6816
+ return 0;
6817
+ const plugin = options.plugin ?? nativeObservability;
6818
+ const pending = await plugin.pending();
6819
+ const reports = pending.reports.slice(0, MAX_REPORTS).map(parseReport).filter((report) => report !== undefined);
6820
+ const discarded = reports.filter((report) => !sampled(report.id, config.sampleRate));
6821
+ if (discarded.length > 0)
6822
+ await plugin.acknowledge({
6823
+ ids: discarded.map(({ id }) => id)
6824
+ });
6825
+ const selected = reports.filter((report) => sampled(report.id, config.sampleRate));
6826
+ if (selected.length === 0)
6827
+ return 0;
6828
+ const envelope = {
6829
+ ...config.environment ? { environment: config.environment } : {},
6830
+ events: selected.map((report) => reportEvent(report, manifest)),
6831
+ project: config.project,
6832
+ release: manifest.appBuild,
6833
+ v: 1
6834
+ };
6835
+ const response = await fetch2(config.endpoint, {
6836
+ body: JSON.stringify(envelope),
6837
+ headers: { "content-type": "application/json" },
6838
+ method: "POST"
6839
+ });
6840
+ if (!response.ok)
6841
+ return 0;
6842
+ await plugin.acknowledge({
6843
+ ids: selected.map(({ id }) => id)
6844
+ });
6845
+ return selected.length;
6846
+ };
6847
+
6713
6848
  // src/mobile/shellBootstrap.ts
6714
6849
  var MANIFEST_PATH = "./absolute-mobile-manifest.json";
6715
6850
  var STATUS_ID = "absolute-mobile-status";
@@ -6956,6 +7091,23 @@ var startAbsoluteMobileShell = async (options = {}) => {
6956
7091
  }) : undefined;
6957
7092
  const applicationFetch = auth?.fetch ?? options.createFetch?.(manifest) ?? globalThis.fetch;
6958
7093
  const observability = installAbsoluteMobileShellObservability(manifest, options.engine ?? "capacitor", applicationFetch);
7094
+ if ((options.engine ?? "capacitor") === "capacitor") {
7095
+ let nativeObservabilityDrain;
7096
+ const drainNativeObservability = () => {
7097
+ nativeObservabilityDrain ??= drainAbsoluteMobileNativeObservability(manifest, applicationFetch).catch(() => 0).finally(() => {
7098
+ nativeObservabilityDrain = undefined;
7099
+ });
7100
+ return nativeObservabilityDrain;
7101
+ };
7102
+ drainNativeObservability();
7103
+ addEventListener("online", () => void drainNativeObservability());
7104
+ App.addListener("appStateChange", ({ isActive }) => {
7105
+ if (isActive)
7106
+ drainNativeObservability();
7107
+ }).catch(() => {
7108
+ return;
7109
+ });
7110
+ }
6959
7111
  installAbsoluteMobileShellHttp(manifest.productionOrigin, applicationFetch);
6960
7112
  if (auth)
6961
7113
  options.connectPush?.(auth);
@@ -0,0 +1,3 @@
1
+ export declare const expoNativeObservabilityRuntimeSource = "// Generated by AbsoluteJS. Edit absolute.config.ts, then run absolute mobile sync.\nimport { requireNativeModule } from 'expo';\nimport { AppState, Platform } from 'react-native';\nimport { ABSOLUTE_MOBILE_MANIFEST } from './webAssets';\n\ntype NativeObservabilityModule = {\n\tacknowledge(options: { ids: string[] }): Promise<void>;\n\tpending(): Promise<{ reports: unknown[] }>;\n};\ntype NativeReport = { details: Record<string, unknown>; id: string; kind: string; occurredAt: number; platform: 'android' | 'ios' };\nconst native = requireNativeModule<NativeObservabilityModule>('AbsoluteMobileObservability');\nconst KINDS = new Set(['anr', 'app-launch', 'cpu-exception', 'crash', 'disk-write-exception', 'hang', 'initialization-failure', 'low-memory', 'native-crash', 'resource-exhaustion', 'signal']);\nconst ID = /^[A-Za-z0-9._:-]{1,128}$/;\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value);\nconst redactText = (value: string) => value.slice(0, 8192)\n\t.replace(/\\b(Bearer)\\s+[A-Za-z0-9._~+/-]+=*/gi, '$1 [REDACTED]')\n\t.replace(/([?&](?:access_token|api_?key|authorization|code|id_token|password|refresh_token|secret|token)=)[^&#\\s]*/gi, '$1[REDACTED]')\n\t.replace(/\\b((?:access_?token|api_?key|authorization|client_?secret|cookie|id_?token|password|passwd|refresh_?token|secret|token)\\s*[:=]\\s*)[^,\\s;]+/gi, '$1[REDACTED]')\n\t.replace(/\\beyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\b/g, '[REDACTED]');\nconst redact = (value: unknown, depth = 0): unknown => {\n\tif (depth >= 8) return '[TRUNCATED]';\n\tif (typeof value === 'string') return redactText(value);\n\tif (value === null || typeof value === 'number' || typeof value === 'boolean') return value;\n\tif (Array.isArray(value)) return value.slice(0, 64).map(entry => redact(entry, depth + 1));\n\tif (!isRecord(value)) return String(value).slice(0, 8192);\n\treturn Object.fromEntries(Object.entries(value).slice(0, 128).map(([key, entry]) => [key, /(?:authorization|cookie|password|secret|token)/i.test(key) ? '[REDACTED]' : redact(entry, depth + 1)]));\n};\nconst parse = (value: unknown): NativeReport | undefined => {\n\tif (!isRecord(value) || !isRecord(value.details) || typeof value.id !== 'string' || !ID.test(value.id) || typeof value.kind !== 'string' || !KINDS.has(value.kind) || typeof value.occurredAt !== 'number' || !Number.isFinite(value.occurredAt) || value.occurredAt <= 0 || value.platform !== 'android' && value.platform !== 'ios') return;\n\tconst details = redact(value.details);\n\tif (!isRecord(details) || new TextEncoder().encode(JSON.stringify(details)).byteLength > 64 * 1024) return;\n\treturn { details, id: value.id, kind: value.kind, occurredAt: value.occurredAt, platform: value.platform };\n};\nconst sampled = (id: string, rate: number) => {\n\tif (rate >= 1) return true;\n\tlet hash = 2166136261;\n\tfor (const character of id) { hash ^= character.codePointAt(0) ?? 0; hash = Math.imul(hash, 16777619); }\n\treturn (hash >>> 0) / 4294967296 < rate;\n};\nlet drainPromise: Promise<void> | undefined;\nlet started = false;\nconst drain = () => {\n\tdrainPromise ??= (async () => {\n\t\tconst manifest = ABSOLUTE_MOBILE_MANIFEST;\n\t\tconst config = manifest?.observability;\n\t\tif (!manifest || !config || Platform.OS !== 'android' && Platform.OS !== 'ios') return;\n\t\tconst reports = (await native.pending()).reports.slice(0, 8).map(parse).filter((report): report is NativeReport => report !== undefined);\n\t\tconst discarded = reports.filter(report => !sampled(report.id, config.sampleRate));\n\t\tif (discarded.length) await native.acknowledge({ ids: discarded.map(report => report.id) });\n\t\tconst selected = reports.filter(report => sampled(report.id, config.sampleRate));\n\t\tif (!selected.length) return;\n\t\tconst response = await fetch(config.endpoint, {\n\t\t\tbody: JSON.stringify({ ...(config.environment ? { environment: config.environment } : {}), events: selected.map(report => ({ at: report.occurredAt, extra: { nativeDiagnostic: report.details, nativeDiagnosticId: report.id }, groupingKey: 'absolute-native:' + report.platform + ':' + report.kind, level: ['app-launch', 'cpu-exception', 'disk-write-exception', 'low-memory', 'resource-exhaustion', 'signal'].includes(report.kind) ? 'warning' : 'error', message: 'Native process diagnostic \u2014 ' + report.kind, name: 'AbsoluteMobileNativeDiagnostic', tags: { absoluteMobile: 'true', mobileAppBuild: manifest.appBuild, mobileEngine: 'expo', mobileFailurePhase: 'native-process', mobileManifestFormat: '1', mobileNativeRuntime: 'expo', mobilePlatform: report.platform, mobileRuntime: manifest.runtime } })), project: config.project, release: manifest.appBuild, v: 1 }),\n\t\t\theaders: { 'content-type': 'application/json' },\n\t\t\tmethod: 'POST'\n\t\t});\n\t\tif (response.ok) await native.acknowledge({ ids: selected.map(report => report.id) });\n\t})().catch(() => undefined).finally(() => { drainPromise = undefined; });\n\treturn drainPromise;\n};\nexport const startAbsoluteExpoNativeObservability = () => {\n\tif (!started) {\n\t\tstarted = true;\n\t\tAppState.addEventListener('change', state => { if (state === 'active') void drain(); });\n\t}\n\treturn drain();\n};\n";
2
+ export declare const absoluteExpoNativeObservabilityFiles: (project: string) => Map<string, string>;
3
+ export declare const absoluteExpoNativeObservabilityPaths: (project: string) => string[];
@@ -21,6 +21,7 @@ export * from './expoBridge';
21
21
  export * from './expoCodeSigning';
22
22
  export * from './expoDevController';
23
23
  export * from './expoProject';
24
+ export * from './expoNativeObservability';
24
25
  export * from './expoUpdate';
25
26
  export * from './capacitorProject';
26
27
  export * from './config';
@@ -34,6 +35,7 @@ export * from './nativeDeviceCapabilities';
34
35
  export * from './nativeAuth';
35
36
  export * from './nativeRoute';
36
37
  export * from './nativeUpdates';
38
+ export * from './nativeObservability';
37
39
  export * from './pageProtocol';
38
40
  export * from './producerContext';
39
41
  export * from './releaseArtifact';
@@ -41,6 +43,7 @@ export * from './releasePublisher';
41
43
  export * from './routeMetadataTransform';
42
44
  export * from './routeMatcher';
43
45
  export * from './shellHttp';
46
+ export * from './shellNativeObservability';
44
47
  export * from './shellObservability';
45
48
  export * from './transport';
46
49
  export * from './uiPrimitives';
@@ -0,0 +1,4 @@
1
+ import type { NormalizedAbsoluteMobileConfig } from './config';
2
+ export declare const applyAbsoluteNativeObservability: (config: NormalizedAbsoluteMobileConfig, platforms?: import("..").MobilePlatform[]) => Promise<{
3
+ changed: boolean;
4
+ }>;
@@ -0,0 +1,21 @@
1
+ import type { AbsoluteMobileClientManifest, AbsoluteMobileFetch } from './transport';
2
+ export type AbsoluteMobileNativeDiagnostic = {
3
+ details: Record<string, unknown>;
4
+ id: string;
5
+ kind: string;
6
+ occurredAt: number;
7
+ platform: 'android' | 'ios';
8
+ };
9
+ export type AbsoluteMobileNativeObservabilityPlugin = {
10
+ acknowledge(options: {
11
+ ids: string[];
12
+ }): Promise<void>;
13
+ pending(): Promise<{
14
+ reports: unknown[];
15
+ }>;
16
+ };
17
+ export type AbsoluteMobileNativeObservabilityDrainOptions = {
18
+ native?: boolean;
19
+ plugin?: AbsoluteMobileNativeObservabilityPlugin;
20
+ };
21
+ export declare const drainAbsoluteMobileNativeObservability: (manifest: AbsoluteMobileClientManifest, fetch: AbsoluteMobileFetch, options?: AbsoluteMobileNativeObservabilityDrainOptions) => Promise<number>;
package/package.json CHANGED
@@ -296,12 +296,12 @@
296
296
  "main": "./dist/index.js",
297
297
  "name": "@absolutejs/absolute",
298
298
  "optionalDependencies": {
299
- "@absolutejs/native-darwin-arm64": "0.20.0-beta.67",
300
- "@absolutejs/native-darwin-x64": "0.20.0-beta.67",
301
- "@absolutejs/native-linux-arm64": "0.20.0-beta.67",
302
- "@absolutejs/native-linux-x64": "0.20.0-beta.67",
303
- "@absolutejs/native-windows-arm64": "0.20.0-beta.67",
304
- "@absolutejs/native-windows-x64": "0.20.0-beta.67"
299
+ "@absolutejs/native-darwin-arm64": "0.20.0-beta.69",
300
+ "@absolutejs/native-darwin-x64": "0.20.0-beta.69",
301
+ "@absolutejs/native-linux-arm64": "0.20.0-beta.69",
302
+ "@absolutejs/native-linux-x64": "0.20.0-beta.69",
303
+ "@absolutejs/native-windows-arm64": "0.20.0-beta.69",
304
+ "@absolutejs/native-windows-x64": "0.20.0-beta.69"
305
305
  },
306
306
  "overrides": {
307
307
  "@sinclair/typebox": "0.34.52",
@@ -463,6 +463,7 @@
463
463
  "test:native:android:bundle": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts",
464
464
  "test:native:android:hmr": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-hmr-conformance.test.ts",
465
465
  "test:native:android:lifecycle": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-lifecycle-conformance.test.ts",
466
+ "test:native:android:observability": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts -t 'native process crash'",
466
467
  "test:native:android:updates": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts -t 'signed web updates'",
467
468
  "test:native:android:upgrade": "ABSOLUTE_TEST_NATIVE_ANDROID=1 bun test tests/native/android-embedded-bundle-conformance.test.ts -t 'installed APK upgrade'",
468
469
  "test:native:ios": "bun run test:native:ios:lifecycle && bun run test:native:ios:bundle",
@@ -519,7 +520,7 @@
519
520
  ]
520
521
  }
521
522
  },
522
- "version": "0.20.0-beta.67",
523
+ "version": "0.20.0-beta.69",
523
524
  "workspaces": [
524
525
  "tests/fixtures/*",
525
526
  "tests/fixtures/_packages/*"