react-native-update 10.54.0 → 10.55.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/README-CN.md CHANGED
@@ -16,6 +16,34 @@
16
16
 
17
17
  请查看[文档](https://pushy.reactnative.cn/docs/getting-started.html)
18
18
 
19
+ ### JS 错误上报
20
+
21
+ SDK 可以把未捕获错误和手动捕获的 JavaScript 错误关联到当前实际运行的热更版本。
22
+ 此能力默认开启、仅做尽力上报,在开发环境或启用 `disableTelemetry` 时不发送;
23
+ 全局处理器采用链式安装,不会取代 React Native、Sentry 或 Crashlytics 已有的处理器。
24
+
25
+ ```ts
26
+ import { Pushy } from 'react-native-update';
27
+
28
+ const client = new Pushy({ appKey: 'your-app-key' });
29
+
30
+ try {
31
+ await submitOrder();
32
+ } catch (error) {
33
+ client.captureException(error, {
34
+ extra: { screen: 'checkout', retry: 1 },
35
+ });
36
+ }
37
+
38
+ // 运行时显式退出(同时关闭手动 captureException 的传输):
39
+ client.setOptions({ disableErrorReporting: true });
40
+ ```
41
+
42
+ 也可以在初始化时通过 `new Pushy({ appKey, disableErrorReporting: true })` 直接退出。
43
+
44
+ 只有携带明确版本 hash 的热更版本错误会被上传。上下文字段只接受经过限长的标量值;
45
+ 不要放入密钥或个人信息。
46
+
19
47
  ### 优势
20
48
 
21
49
  1. 对中国用户使用阿里云高速 CDN 分发,对比其他服务器在国外的热更新服务,分发更稳定,更新成功率极高。海外用户智能分流至 cloudflare,同样提供稳定高速的分发体验。
package/README.md CHANGED
@@ -14,6 +14,37 @@ See the docs:
14
14
 
15
15
  - English docs: <https://cresc.dev/docs/getting-started>
16
16
 
17
+ ## JS error reporting
18
+
19
+ The SDK reports uncaught and manually captured JavaScript errors against the
20
+ currently running OTA release. Reporting is enabled by default, best-effort,
21
+ disabled in development and when `disableTelemetry` is enabled, and never
22
+ replaces an existing React Native, Sentry or Crashlytics global handler.
23
+
24
+ ```ts
25
+ import { Cresc } from 'react-native-update';
26
+
27
+ const client = new Cresc({ appKey: 'your-app-key' });
28
+
29
+ try {
30
+ await submitOrder();
31
+ } catch (error) {
32
+ client.captureException(error, {
33
+ extra: { screen: 'checkout', retry: 1 },
34
+ });
35
+ }
36
+
37
+ // Explicit runtime opt-out (also disables manual captureException transport):
38
+ client.setOptions({ disableErrorReporting: true });
39
+ ```
40
+
41
+ You can also opt out at construction with
42
+ `new Cresc({ appKey, disableErrorReporting: true })`.
43
+
44
+ Only errors from an OTA version with a known version hash are uploaded. Context
45
+ is deliberately restricted to bounded scalar values; do not include secrets or
46
+ personal information.
47
+
17
48
  ## Advantages
18
49
 
19
50
  1. react-native-update provides a dedicated global service with fast and reliable worldwide delivery.
@@ -85,4 +116,3 @@ Since 10.52.1 every cold start runs one background update check that **does not
85
116
  | **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
86
117
  | **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
87
118
  | **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
88
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.54.0",
3
+ "version": "10.55.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
package/src/client.ts CHANGED
@@ -25,6 +25,13 @@ import {
25
25
  UpdateError,
26
26
  type UpdateErrorCode,
27
27
  } from './error';
28
+ import {
29
+ type ErrorReportContext,
30
+ type ErrorReportingOptions,
31
+ installGlobalErrorHandler,
32
+ type SerializedException,
33
+ serializeException,
34
+ } from './errorReporting';
28
35
  import i18n from './i18n';
29
36
  import {
30
37
  resolveServerEventHash,
@@ -78,10 +85,6 @@ const SERVER_PRESETS = {
78
85
  main: [
79
86
  'https://update.react-native.cn/api',
80
87
  'https://update.reactnative.cn/api',
81
- 'https://1.rnupdate.online/api',
82
- 'https://2.rnupdate.online/api',
83
- 'https://3.rnupdate.online/api',
84
- 'https://4.rnupdate.online/api',
85
88
  ],
86
89
  queryUrls: [
87
90
  'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json',
@@ -125,6 +128,7 @@ const createDefaultClientOptions = (): ClientOptions => ({
125
128
  logger: noop,
126
129
  debug: false,
127
130
  throwError: false,
131
+ disableErrorReporting: false,
128
132
  });
129
133
 
130
134
  export const sharedState: {
@@ -182,6 +186,8 @@ export class Pushy {
182
186
  private pendingNativeConfigJson?: string;
183
187
  private nativeConfigSyncInFlight = false;
184
188
  private reportedInvalidUpdates = new Set<string>();
189
+ private reportedExceptions = new WeakSet<object>();
190
+ private globalErrorCleanup?: () => void;
185
191
 
186
192
  version = cInfo.rnu;
187
193
  loggerPromise = (() => {
@@ -280,9 +286,18 @@ export class Pushy {
280
286
  log('onOptionsChange listener error:', e?.message || e);
281
287
  }
282
288
  }
289
+ this.syncAutomaticErrorReporting();
283
290
  this.syncNativeConfig();
284
291
  };
285
292
 
293
+ private syncAutomaticErrorReporting = (): void => {
294
+ if (this.options.disableTelemetry || this.options.disableErrorReporting) {
295
+ this.globalErrorCleanup?.();
296
+ return;
297
+ }
298
+ this.enableErrorReporting();
299
+ };
300
+
286
301
  /** Build the subset of options used by the native cold-start check. */
287
302
  private getNativeConfig = (): Record<string, unknown> | undefined => {
288
303
  if (
@@ -518,6 +533,115 @@ export class Pushy {
518
533
  });
519
534
  };
520
535
 
536
+ /**
537
+ * Best-effort JS exception reporting for the currently running OTA release.
538
+ * It is intentionally synchronous to call and never throws; the transport
539
+ * runs in the background and preserves the app's existing error flow.
540
+ */
541
+ captureException = (
542
+ error: unknown,
543
+ context: ErrorReportContext = {}
544
+ ): void => {
545
+ try {
546
+ if (
547
+ __DEV__ ||
548
+ this.options.disableTelemetry ||
549
+ this.options.disableErrorReporting
550
+ ) {
551
+ return;
552
+ }
553
+ if (
554
+ error !== null &&
555
+ (typeof error === 'object' || typeof error === 'function')
556
+ ) {
557
+ const object = error as object;
558
+ if (this.reportedExceptions.has(object)) {
559
+ return;
560
+ }
561
+ this.reportedExceptions.add(object);
562
+ }
563
+ this.reportExceptionToServer(serializeException(error, context));
564
+ } catch (e: any) {
565
+ log('exception telemetry error:', e?.message || e);
566
+ }
567
+ };
568
+
569
+ /**
570
+ * Ensure uncaught JS exception capture is installed and return a runtime
571
+ * cleanup function. Capture is installed automatically by default; this
572
+ * method remains an idempotent compatibility hook. The wrapper always
573
+ * chains the handler that was installed before it and only restores that
574
+ * handler when no later integration has replaced ours.
575
+ */
576
+ enableErrorReporting = (
577
+ options: ErrorReportingOptions = {}
578
+ ): (() => void) => {
579
+ if (
580
+ options.captureGlobal === false ||
581
+ this.options.disableTelemetry ||
582
+ this.options.disableErrorReporting
583
+ ) {
584
+ return () => {};
585
+ }
586
+ if (this.globalErrorCleanup) {
587
+ return this.globalErrorCleanup;
588
+ }
589
+ const uninstall = installGlobalErrorHandler((error, isFatal) => {
590
+ this.captureException(error, { fatal: isFatal === true });
591
+ });
592
+ let active = true;
593
+ const cleanup = () => {
594
+ if (!active) {
595
+ return;
596
+ }
597
+ active = false;
598
+ uninstall();
599
+ if (this.globalErrorCleanup === cleanup) {
600
+ this.globalErrorCleanup = undefined;
601
+ }
602
+ };
603
+ this.globalErrorCleanup = cleanup;
604
+ return cleanup;
605
+ };
606
+
607
+ private reportExceptionToServer = (error: SerializedException): void => {
608
+ try {
609
+ if (
610
+ __DEV__ ||
611
+ this.options.disableTelemetry ||
612
+ this.options.disableErrorReporting ||
613
+ !currentVersion
614
+ ) {
615
+ return;
616
+ }
617
+ const { appKey } = this.options;
618
+ const endpoint =
619
+ this.lastWorkingEndpoint || this.options.server?.main?.[0];
620
+ if (!appKey || !endpoint) {
621
+ return;
622
+ }
623
+ fetchWithTimeout(
624
+ `${endpoint}/report/${appKey}`,
625
+ {
626
+ method: 'POST',
627
+ headers: { 'Content-Type': 'application/json' },
628
+ body: JSON.stringify({
629
+ type: 'js_exception',
630
+ hash: currentVersion,
631
+ packageVersion: this.getEffectivePackageVersion(),
632
+ cInfo,
633
+ error,
634
+ }),
635
+ },
636
+ DEFAULT_FETCH_TIMEOUT_MS
637
+ ).catch((e: any) => {
638
+ log('exception telemetry report failed:', e?.message || e);
639
+ });
640
+ } catch (e: any) {
641
+ log('exception telemetry error:', e?.message || e);
642
+ }
643
+ };
644
+
521
645
  report = async ({
522
646
  type,
523
647
  message = '',
@@ -0,0 +1,136 @@
1
+ export type ErrorContextValue = string | number | boolean | null;
2
+
3
+ export interface ErrorReportContext {
4
+ fatal?: boolean;
5
+ componentStack?: string;
6
+ extra?: Record<string, ErrorContextValue>;
7
+ }
8
+
9
+ export interface ErrorReportingOptions {
10
+ /** Install a chained React Native ErrorUtils handler. Default: true. */
11
+ captureGlobal?: boolean;
12
+ }
13
+
14
+ export interface SerializedException {
15
+ name: string;
16
+ message: string;
17
+ stack: string;
18
+ fatal: boolean;
19
+ componentStack?: string;
20
+ extra?: Record<string, ErrorContextValue>;
21
+ }
22
+
23
+ const MAX_NAME = 128;
24
+ const MAX_MESSAGE = 2 * 1024;
25
+ const MAX_STACK = 32 * 1024;
26
+ const MAX_COMPONENT_STACK = 16 * 1024;
27
+ const MAX_EXTRA_FIELDS = 32;
28
+ const MAX_EXTRA_KEY = 64;
29
+ const MAX_EXTRA_STRING = 1024;
30
+ const MAX_EXTRA_JSON = 8 * 1024;
31
+
32
+ const boundedString = (value: unknown, maximum: number) =>
33
+ typeof value === 'string' ? value.slice(0, maximum) : '';
34
+
35
+ const normalizeExtra = (
36
+ extra?: Record<string, ErrorContextValue>
37
+ ): Record<string, ErrorContextValue> | undefined => {
38
+ if (!extra || typeof extra !== 'object') {
39
+ return undefined;
40
+ }
41
+ const result: Record<string, ErrorContextValue> = {};
42
+ for (const [rawKey, value] of Object.entries(extra).slice(
43
+ 0,
44
+ MAX_EXTRA_FIELDS
45
+ )) {
46
+ const key = rawKey.slice(0, MAX_EXTRA_KEY);
47
+ if (!key) {
48
+ continue;
49
+ }
50
+ if (typeof value === 'string') {
51
+ result[key] = value.slice(0, MAX_EXTRA_STRING);
52
+ } else if (
53
+ value === null ||
54
+ typeof value === 'boolean' ||
55
+ (typeof value === 'number' && Number.isFinite(value))
56
+ ) {
57
+ result[key] = value;
58
+ }
59
+ // Keep the client comfortably inside the server's byte cap. The server
60
+ // remains authoritative because JS string length is not UTF-8 byte length.
61
+ if (JSON.stringify(result).length > MAX_EXTRA_JSON) {
62
+ delete result[key];
63
+ break;
64
+ }
65
+ }
66
+ return Object.keys(result).length > 0 ? result : undefined;
67
+ };
68
+
69
+ export const serializeException = (
70
+ error: unknown,
71
+ context: ErrorReportContext = {}
72
+ ): SerializedException => {
73
+ const source =
74
+ error !== null && (typeof error === 'object' || typeof error === 'function')
75
+ ? (error as Record<string, unknown>)
76
+ : undefined;
77
+ const name = boundedString(source?.name, MAX_NAME).trim() || 'Error';
78
+ const message = (
79
+ boundedString(source?.message, MAX_MESSAGE) || String(error ?? '')
80
+ ).slice(0, MAX_MESSAGE);
81
+ const stack =
82
+ boundedString(source?.stack, MAX_STACK) || `${name}: ${message}`;
83
+ const componentStack = boundedString(
84
+ context.componentStack,
85
+ MAX_COMPONENT_STACK
86
+ );
87
+ const extra = normalizeExtra(context.extra);
88
+ return {
89
+ name,
90
+ message,
91
+ stack,
92
+ fatal: context.fatal === true,
93
+ ...(componentStack ? { componentStack } : {}),
94
+ ...(extra ? { extra } : {}),
95
+ };
96
+ };
97
+
98
+ type GlobalErrorHandler = (error: unknown, isFatal?: boolean) => void;
99
+
100
+ interface GlobalErrorUtils {
101
+ getGlobalHandler?: () => GlobalErrorHandler;
102
+ setGlobalHandler?: (handler: GlobalErrorHandler) => void;
103
+ }
104
+
105
+ /**
106
+ * Install a React Native global error wrapper without taking ownership away
107
+ * from ExceptionsManager, Sentry, Crashlytics, or a handler installed later.
108
+ */
109
+ export const installGlobalErrorHandler = (
110
+ capture: GlobalErrorHandler
111
+ ): (() => void) => {
112
+ const errorUtils = (globalThis as any).ErrorUtils as
113
+ | GlobalErrorUtils
114
+ | undefined;
115
+ if (
116
+ typeof errorUtils?.getGlobalHandler !== 'function' ||
117
+ typeof errorUtils.setGlobalHandler !== 'function'
118
+ ) {
119
+ return () => {};
120
+ }
121
+ const previous = errorUtils.getGlobalHandler();
122
+ const wrapper: GlobalErrorHandler = (error, isFatal) => {
123
+ try {
124
+ capture(error, isFatal);
125
+ } catch {
126
+ // Reporting must never alter React Native's original failure path.
127
+ }
128
+ previous?.(error, isFatal);
129
+ };
130
+ errorUtils.setGlobalHandler(wrapper);
131
+ return () => {
132
+ if (errorUtils.getGlobalHandler?.() === wrapper) {
133
+ errorUtils.setGlobalHandler?.(previous);
134
+ }
135
+ };
136
+ };
package/src/index.ts CHANGED
@@ -10,6 +10,12 @@ export {
10
10
  export { PushyModule, UpdateModule } from './core';
11
11
  export type { UpdateErrorCode } from './error';
12
12
  export { UpdateError } from './error';
13
+ export type {
14
+ ErrorContextValue,
15
+ ErrorReportContext,
16
+ ErrorReportingOptions,
17
+ SerializedException,
18
+ } from './errorReporting';
13
19
  export type { CrashReporterLike, UpdateMetadata } from './metadata';
14
20
  export {
15
21
  attachToCrashlytics,
package/src/type.ts CHANGED
@@ -205,6 +205,13 @@ export interface ClientOptions {
205
205
  * the version health view in the console. Default: false (enabled).
206
206
  */
207
207
  disableTelemetry?: boolean;
208
+ /**
209
+ * Disable automatic and manual JavaScript exception reporting. Reporting is
210
+ * enabled by default and chains React Native's existing global ErrorUtils
211
+ * handler. Set this to true for an explicit opt-out; update lifecycle
212
+ * telemetry remains controlled separately by `disableTelemetry`.
213
+ */
214
+ disableErrorReporting?: boolean;
208
215
  /**
209
216
  * Disable the native cold-start update check: the background check that runs
210
217
  * a few seconds after every launch, independent of JS