@observerkit/metro 0.2.0 → 0.2.1

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.md CHANGED
@@ -26,7 +26,7 @@ The `observerkit-upload` CLI reads the project key from `--project-key` or the `
26
26
 
27
27
  ### Expo
28
28
 
29
- Add the config plugin; prebuild wires both native builds automatically:
29
+ Add the config plugin; prebuild wires both native builds automatically, including the Android R8 mapping upload described below:
30
30
 
31
31
  ```json
32
32
  {
@@ -94,6 +94,18 @@ tasks.configureEach { task ->
94
94
 
95
95
  The composed map lands in `generated/sourcemaps/react/<variant>/`, while the pre-compose packager map (needed to restore the `debugId`, see Hermes below) stays behind in `intermediates/sourcemaps/react/<variant>/`. `ignoreExitValue` plus the logged warning make sure a failed upload never fails `assembleRelease`.
96
96
 
97
+ ## Android R8/ProGuard mapping upload
98
+
99
+ Release Android builds run through R8/ProGuard, so JVM crash and ANR stack traces are obfuscated unless a mapping file is uploaded. The Expo config plugin wires this automatically: a Gradle hook runs after the release R8 minify task (`minify<Flavor>ReleaseWithR8`) and uploads `mapping.txt`, keyed by `versionName` and `versionCode` read from `android.defaultConfig`. The hook is release-only, mirroring the source map hook above: every variant shares `defaultConfig`'s version pair, and the server stores mappings last-upload-wins, so a minified debug or staging build would otherwise clobber the release mapping for the same version. Rebuilding the same version overwrites the stored mapping, so re-upload happens naturally on every release build.
100
+
101
+ For bare React Native, or apps that override the version per flavor, upload the mapping manually with the CLI:
102
+
103
+ ```bash
104
+ observerkit-upload --mapping android/app/build/outputs/mapping/release/mapping.txt --version-name 1.2.0 --version-code 42
105
+ ```
106
+
107
+ The server retraces JVM crash and ANR frames with the uploaded mapping before grouping, so deobfuscated frames stay grouped consistently across releases.
108
+
97
109
  ## Hermes
98
110
 
99
111
  Symbolication support targets Hermes, the React Native default JS engine; JSC is not supported.
@@ -108,9 +120,14 @@ Hermes release builds compose the Metro source map with the bytecode map, which
108
120
  ```
109
121
  observerkit-upload --map <path> [--packager-map <path>] [--bundle <path>]
110
122
  observerkit-upload --dir <path>
111
-
112
- --project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
113
- --endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or https://ingest.observerkit.com
123
+ observerkit-upload --mapping <path> --version-name <name> --version-code <code>
124
+
125
+ --project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
126
+ --endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or https://ingest.observerkit.com
127
+ --mapping <path> R8/ProGuard mapping.txt to upload, keyed by
128
+ --version-name and --version-code
129
+ --version-name <name> App versionName, required with --mapping
130
+ --version-code <code> App versionCode (digits only), required with --mapping
114
131
  ```
115
132
 
116
133
  ## pnpm note
@@ -10,6 +10,14 @@ function isJsonObject(value) {
10
10
  // ../plugin-core/src/upload.ts
11
11
  var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
12
12
  var DEFAULT_MAX_BATCH_BYTES = 100 * 1024 * 1024;
13
+ var RETRY_DELAYS_MS = [1e3, 4e3];
14
+ var sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
15
+ function isDirectoryMissing(error) {
16
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
17
+ }
18
+ function isCssMap(mapFile) {
19
+ return mapFile.endsWith(".css.map");
20
+ }
13
21
  function hasDebugId(content) {
14
22
  if (!content.includes('"debugId"')) return false;
15
23
  try {
@@ -49,6 +57,18 @@ async function deleteMapFiles(directory, mapFiles) {
49
57
  `[ObserverKit] Deleted ${mapFiles.length - failures.length} .map files from output`
50
58
  );
51
59
  }
60
+ var UploadResponseError = class extends Error {
61
+ status;
62
+ constructor(status, message) {
63
+ super(message);
64
+ this.name = "UploadResponseError";
65
+ this.status = status;
66
+ }
67
+ };
68
+ function isTransient(error) {
69
+ if (error instanceof UploadResponseError) return error.status >= 500;
70
+ return error instanceof TypeError;
71
+ }
52
72
  async function uploadBatch(batch, options) {
53
73
  const formData = new FormData();
54
74
  for (const [index, entry] of batch.entries()) {
@@ -70,11 +90,28 @@ async function uploadBatch(batch, options) {
70
90
  });
71
91
  if (!response.ok) {
72
92
  const text = await response.text();
73
- throw new Error(
93
+ throw new UploadResponseError(
94
+ response.status,
74
95
  `[ObserverKit] Source map upload failed: ${response.status} ${text}`
75
96
  );
76
97
  }
77
98
  }
99
+ async function uploadBatchWithRetry(batch, options) {
100
+ for (let attempt = 0; ; attempt++) {
101
+ try {
102
+ await uploadBatch(batch, options);
103
+ return;
104
+ } catch (err) {
105
+ const wait = RETRY_DELAYS_MS[attempt];
106
+ if (wait === void 0 || !isTransient(err)) throw err;
107
+ const reason = err instanceof UploadResponseError ? `(${err.status})` : "(network error)";
108
+ console.warn(
109
+ `[ObserverKit] Upload failed ${reason}, retrying in ${wait / 1e3}s (attempt ${attempt + 2}/${RETRY_DELAYS_MS.length + 1})`
110
+ );
111
+ await options.delay(wait);
112
+ }
113
+ }
114
+ }
78
115
  var JS_SOURCE_MAPPING_URL = /^\/\/[#@] sourceMappingURL=\S+[ \t]*$/gm;
79
116
  var CSS_SOURCE_MAPPING_URL = /^\/\*# sourceMappingURL=\S+[ \t]*\*\/[ \t]*$/gm;
80
117
  function isChunkFile(name) {
@@ -105,10 +142,28 @@ async function uploadSourceMaps(options) {
105
142
  const { projectKey, directory, endpoint, deleteAfterUpload, pluginVersion, pluginName } = options;
106
143
  const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES;
107
144
  const maxBatchBytes = options.maxBatchBytes ?? DEFAULT_MAX_BATCH_BYTES;
108
- const allFiles = options.files ? [...options.files] : await readdir(directory, { recursive: true, encoding: "utf-8" });
145
+ const delay = options.delay ?? sleep;
146
+ let allFiles;
147
+ if (options.files) {
148
+ allFiles = [...options.files];
149
+ } else {
150
+ try {
151
+ allFiles = await readdir(directory, { recursive: true, encoding: "utf-8" });
152
+ } catch (err) {
153
+ if (isDirectoryMissing(err)) {
154
+ console.error(
155
+ `[ObserverKit] Output directory not found: ${directory}. No source maps were uploaded. Check that this is where your build writes its output.`
156
+ );
157
+ return;
158
+ }
159
+ throw err;
160
+ }
161
+ }
109
162
  const allMapFiles = allFiles.filter((f) => f.endsWith(".map"));
110
163
  if (allMapFiles.length === 0) {
111
- console.log("[ObserverKit] No .map files found, skipping source map upload");
164
+ console.warn(
165
+ `[ObserverKit] No .map files found in ${directory}, skipping source map upload. Source map emission is enabled by this plugin, so an empty scan usually means the build wrote its output elsewhere.`
166
+ );
112
167
  return;
113
168
  }
114
169
  try {
@@ -125,7 +180,7 @@ async function uploadSourceMaps(options) {
125
180
  continue;
126
181
  }
127
182
  if (!hasDebugId(content)) {
128
- skippedNoDebugId += 1;
183
+ if (!isCssMap(mapFile)) skippedNoDebugId += 1;
129
184
  continue;
130
185
  }
131
186
  if (content.byteLength > maxFileBytes) {
@@ -158,11 +213,23 @@ async function uploadSourceMaps(options) {
158
213
  const url = `${endpoint.replace(/\/+$/, "")}/v1/sourcemaps`;
159
214
  const batches = batchBySize(uploadable, maxBatchBytes);
160
215
  for (const [index, batch] of batches.entries()) {
161
- await uploadBatch(batch, { directory, url, projectKey, pluginVersion, pluginName });
162
- console.log(
163
- `[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
164
- );
216
+ await uploadBatchWithRetry(batch, {
217
+ directory,
218
+ url,
219
+ projectKey,
220
+ pluginVersion,
221
+ pluginName,
222
+ delay
223
+ });
224
+ if (batches.length > 1) {
225
+ console.log(
226
+ `[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
227
+ );
228
+ }
165
229
  }
230
+ console.log(
231
+ `[ObserverKit] Uploaded ${uploadable.length} source map(s) from ${directory}`
232
+ );
166
233
  } catch (err) {
167
234
  if (deleteAfterUpload) {
168
235
  console.warn(
@@ -178,6 +245,44 @@ async function uploadSourceMaps(options) {
178
245
  }
179
246
  }
180
247
 
248
+ // ../plugin-core/src/upload-mapping.ts
249
+ import { readFile as readFile2 } from "fs/promises";
250
+ var DEFAULT_MAX_MAPPING_BYTES = 50 * 1024 * 1024;
251
+ async function uploadProguardMapping(options) {
252
+ const maxFileBytes = options.maxFileBytes ?? DEFAULT_MAX_MAPPING_BYTES;
253
+ const content = await readFile2(options.mappingPath);
254
+ if (content.byteLength > maxFileBytes) {
255
+ throw new Error(
256
+ `[ObserverKit] Mapping file exceeds the per-file ingest limit (${maxFileBytes} bytes)`
257
+ );
258
+ }
259
+ const formData = new FormData();
260
+ formData.append("versionName", options.versionName);
261
+ formData.append("versionCode", options.versionCode);
262
+ formData.append(
263
+ "file",
264
+ new Blob([new Uint8Array(content)], { type: "text/plain" }),
265
+ "mapping.txt"
266
+ );
267
+ const url = `${options.endpoint.replace(/\/+$/, "")}/v1/mappings`;
268
+ const response = await fetch(url, {
269
+ method: "POST",
270
+ headers: {
271
+ "X-ObserverKit-Key": options.projectKey,
272
+ "X-ObserverKit-Plugin-Version": options.pluginVersion,
273
+ "X-ObserverKit-Plugin-Name": options.pluginName
274
+ },
275
+ body: formData
276
+ });
277
+ if (!response.ok) {
278
+ const text = await response.text();
279
+ throw new Error(`[ObserverKit] Mapping upload failed: ${response.status} ${text}`);
280
+ }
281
+ console.log(
282
+ `[ObserverKit] Uploaded R8 mapping for version ${options.versionName} (${options.versionCode})`
283
+ );
284
+ }
285
+
181
286
  // ../plugin-core/src/debug-id.ts
182
287
  import { v5 } from "uuid";
183
288
  var OBSERVERKIT_NAMESPACE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d";
@@ -195,5 +300,6 @@ export {
195
300
  generateDebugId,
196
301
  isJsonObject,
197
302
  buildStackKeyedDebugIdSnippet,
198
- uploadSourceMaps
303
+ uploadSourceMaps,
304
+ uploadProguardMapping
199
305
  };
@@ -10,6 +10,14 @@ function isJsonObject(value) {
10
10
  // ../plugin-core/src/upload.ts
11
11
  var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
12
12
  var DEFAULT_MAX_BATCH_BYTES = 100 * 1024 * 1024;
13
+ var RETRY_DELAYS_MS = [1e3, 4e3];
14
+ var sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
15
+ function isDirectoryMissing(error) {
16
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
17
+ }
18
+ function isCssMap(mapFile) {
19
+ return mapFile.endsWith(".css.map");
20
+ }
13
21
  function hasDebugId(content) {
14
22
  if (!content.includes('"debugId"')) return false;
15
23
  try {
@@ -49,6 +57,18 @@ async function deleteMapFiles(directory, mapFiles) {
49
57
  `[ObserverKit] Deleted ${mapFiles.length - failures.length} .map files from output`
50
58
  );
51
59
  }
60
+ var UploadResponseError = class extends Error {
61
+
62
+ constructor(status, message) {
63
+ super(message);
64
+ this.name = "UploadResponseError";
65
+ this.status = status;
66
+ }
67
+ };
68
+ function isTransient(error) {
69
+ if (error instanceof UploadResponseError) return error.status >= 500;
70
+ return error instanceof TypeError;
71
+ }
52
72
  async function uploadBatch(batch, options) {
53
73
  const formData = new FormData();
54
74
  for (const [index, entry] of batch.entries()) {
@@ -70,11 +90,28 @@ async function uploadBatch(batch, options) {
70
90
  });
71
91
  if (!response.ok) {
72
92
  const text = await response.text();
73
- throw new Error(
93
+ throw new UploadResponseError(
94
+ response.status,
74
95
  `[ObserverKit] Source map upload failed: ${response.status} ${text}`
75
96
  );
76
97
  }
77
98
  }
99
+ async function uploadBatchWithRetry(batch, options) {
100
+ for (let attempt = 0; ; attempt++) {
101
+ try {
102
+ await uploadBatch(batch, options);
103
+ return;
104
+ } catch (err) {
105
+ const wait = RETRY_DELAYS_MS[attempt];
106
+ if (wait === void 0 || !isTransient(err)) throw err;
107
+ const reason = err instanceof UploadResponseError ? `(${err.status})` : "(network error)";
108
+ console.warn(
109
+ `[ObserverKit] Upload failed ${reason}, retrying in ${wait / 1e3}s (attempt ${attempt + 2}/${RETRY_DELAYS_MS.length + 1})`
110
+ );
111
+ await options.delay(wait);
112
+ }
113
+ }
114
+ }
78
115
  var JS_SOURCE_MAPPING_URL = /^\/\/[#@] sourceMappingURL=\S+[ \t]*$/gm;
79
116
  var CSS_SOURCE_MAPPING_URL = /^\/\*# sourceMappingURL=\S+[ \t]*\*\/[ \t]*$/gm;
80
117
  function isChunkFile(name) {
@@ -105,10 +142,28 @@ async function uploadSourceMaps(options) {
105
142
  const { projectKey, directory, endpoint, deleteAfterUpload, pluginVersion, pluginName } = options;
106
143
  const maxFileBytes = _nullishCoalesce(options.maxFileBytes, () => ( DEFAULT_MAX_FILE_BYTES));
107
144
  const maxBatchBytes = _nullishCoalesce(options.maxBatchBytes, () => ( DEFAULT_MAX_BATCH_BYTES));
108
- const allFiles = options.files ? [...options.files] : await _promises.readdir.call(void 0, directory, { recursive: true, encoding: "utf-8" });
145
+ const delay = _nullishCoalesce(options.delay, () => ( sleep));
146
+ let allFiles;
147
+ if (options.files) {
148
+ allFiles = [...options.files];
149
+ } else {
150
+ try {
151
+ allFiles = await _promises.readdir.call(void 0, directory, { recursive: true, encoding: "utf-8" });
152
+ } catch (err) {
153
+ if (isDirectoryMissing(err)) {
154
+ console.error(
155
+ `[ObserverKit] Output directory not found: ${directory}. No source maps were uploaded. Check that this is where your build writes its output.`
156
+ );
157
+ return;
158
+ }
159
+ throw err;
160
+ }
161
+ }
109
162
  const allMapFiles = allFiles.filter((f) => f.endsWith(".map"));
110
163
  if (allMapFiles.length === 0) {
111
- console.log("[ObserverKit] No .map files found, skipping source map upload");
164
+ console.warn(
165
+ `[ObserverKit] No .map files found in ${directory}, skipping source map upload. Source map emission is enabled by this plugin, so an empty scan usually means the build wrote its output elsewhere.`
166
+ );
112
167
  return;
113
168
  }
114
169
  try {
@@ -125,7 +180,7 @@ async function uploadSourceMaps(options) {
125
180
  continue;
126
181
  }
127
182
  if (!hasDebugId(content)) {
128
- skippedNoDebugId += 1;
183
+ if (!isCssMap(mapFile)) skippedNoDebugId += 1;
129
184
  continue;
130
185
  }
131
186
  if (content.byteLength > maxFileBytes) {
@@ -158,11 +213,23 @@ async function uploadSourceMaps(options) {
158
213
  const url = `${endpoint.replace(/\/+$/, "")}/v1/sourcemaps`;
159
214
  const batches = batchBySize(uploadable, maxBatchBytes);
160
215
  for (const [index, batch] of batches.entries()) {
161
- await uploadBatch(batch, { directory, url, projectKey, pluginVersion, pluginName });
162
- console.log(
163
- `[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
164
- );
216
+ await uploadBatchWithRetry(batch, {
217
+ directory,
218
+ url,
219
+ projectKey,
220
+ pluginVersion,
221
+ pluginName,
222
+ delay
223
+ });
224
+ if (batches.length > 1) {
225
+ console.log(
226
+ `[ObserverKit] Uploaded batch ${index + 1}/${batches.length} (${batch.length} source maps)`
227
+ );
228
+ }
165
229
  }
230
+ console.log(
231
+ `[ObserverKit] Uploaded ${uploadable.length} source map(s) from ${directory}`
232
+ );
166
233
  } catch (err) {
167
234
  if (deleteAfterUpload) {
168
235
  console.warn(
@@ -178,6 +245,44 @@ async function uploadSourceMaps(options) {
178
245
  }
179
246
  }
180
247
 
248
+ // ../plugin-core/src/upload-mapping.ts
249
+
250
+ var DEFAULT_MAX_MAPPING_BYTES = 50 * 1024 * 1024;
251
+ async function uploadProguardMapping(options) {
252
+ const maxFileBytes = _nullishCoalesce(options.maxFileBytes, () => ( DEFAULT_MAX_MAPPING_BYTES));
253
+ const content = await _promises.readFile.call(void 0, options.mappingPath);
254
+ if (content.byteLength > maxFileBytes) {
255
+ throw new Error(
256
+ `[ObserverKit] Mapping file exceeds the per-file ingest limit (${maxFileBytes} bytes)`
257
+ );
258
+ }
259
+ const formData = new FormData();
260
+ formData.append("versionName", options.versionName);
261
+ formData.append("versionCode", options.versionCode);
262
+ formData.append(
263
+ "file",
264
+ new Blob([new Uint8Array(content)], { type: "text/plain" }),
265
+ "mapping.txt"
266
+ );
267
+ const url = `${options.endpoint.replace(/\/+$/, "")}/v1/mappings`;
268
+ const response = await fetch(url, {
269
+ method: "POST",
270
+ headers: {
271
+ "X-ObserverKit-Key": options.projectKey,
272
+ "X-ObserverKit-Plugin-Version": options.pluginVersion,
273
+ "X-ObserverKit-Plugin-Name": options.pluginName
274
+ },
275
+ body: formData
276
+ });
277
+ if (!response.ok) {
278
+ const text = await response.text();
279
+ throw new Error(`[ObserverKit] Mapping upload failed: ${response.status} ${text}`);
280
+ }
281
+ console.log(
282
+ `[ObserverKit] Uploaded R8 mapping for version ${options.versionName} (${options.versionCode})`
283
+ );
284
+ }
285
+
181
286
  // ../plugin-core/src/debug-id.ts
182
287
  var _uuid = require('uuid');
183
288
  var OBSERVERKIT_NAMESPACE = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d";
@@ -196,4 +301,5 @@ function buildStackKeyedDebugIdSnippet(debugId) {
196
301
 
197
302
 
198
303
 
199
- exports.generateDebugId = generateDebugId; exports.isJsonObject = isJsonObject; exports.buildStackKeyedDebugIdSnippet = buildStackKeyedDebugIdSnippet; exports.uploadSourceMaps = uploadSourceMaps;
304
+
305
+ exports.generateDebugId = generateDebugId; exports.isJsonObject = isJsonObject; exports.buildStackKeyedDebugIdSnippet = buildStackKeyedDebugIdSnippet; exports.uploadSourceMaps = uploadSourceMaps; exports.uploadProguardMapping = uploadProguardMapping;
package/dist/cli.cjs CHANGED
@@ -2,7 +2,8 @@
2
2
  "use strict"; function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
3
3
 
4
4
 
5
- var _chunk6RJOM2COcjs = require('./chunk-6RJOM2CO.cjs');
5
+
6
+ var _chunkDFKRMIQJcjs = require('./chunk-DFKRMIQJ.cjs');
6
7
  require('./chunk-AUOVSTQX.cjs');
7
8
 
8
9
  // src/cli.ts
@@ -31,7 +32,7 @@ async function readDebugIdFromMap(path) {
31
32
  } catch (e2) {
32
33
  return null;
33
34
  }
34
- if (!_chunk6RJOM2COcjs.isJsonObject.call(void 0, parsed)) return null;
35
+ if (!_chunkDFKRMIQJcjs.isJsonObject.call(void 0, parsed)) return null;
35
36
  return typeof parsed["debugId"] === "string" ? parsed["debugId"] : null;
36
37
  }
37
38
  async function readDebugIdFromBundle(path) {
@@ -52,7 +53,7 @@ async function repairDebugId(mapPath, packagerMapPath, bundlePath) {
52
53
  } catch (e4) {
53
54
  throw new Error(`[ObserverKit] ${mapPath} is not valid JSON`);
54
55
  }
55
- if (!_chunk6RJOM2COcjs.isJsonObject.call(void 0, parsed)) {
56
+ if (!_chunkDFKRMIQJcjs.isJsonObject.call(void 0, parsed)) {
56
57
  throw new Error(`[ObserverKit] ${mapPath} is not a source map object`);
57
58
  }
58
59
  if (typeof parsed["debugId"] === "string") return;
@@ -71,10 +72,11 @@ async function repairDebugId(mapPath, packagerMapPath, bundlePath) {
71
72
  }
72
73
 
73
74
  // src/cli.ts
74
- var PLUGIN_VERSION = "0.2.0";
75
+ var PLUGIN_VERSION = "0.2.1";
75
76
  var USAGE = `Usage:
76
77
  observerkit-upload --map <path> [--packager-map <path>] [--bundle <path>]
77
78
  observerkit-upload --dir <path>
79
+ observerkit-upload --mapping <path> --version-name <name> --version-code <code>
78
80
 
79
81
  Options:
80
82
  --map <path> Final source map to upload (repairs debugId first)
@@ -84,6 +86,11 @@ Options:
84
86
  packager map is available (Hermes iOS builds)
85
87
  --dir <path> Upload every debug-id source map in a directory
86
88
  (e.g. an expo export output)
89
+ --mapping <path> R8/ProGuard mapping.txt to upload, keyed by
90
+ --version-name and --version-code
91
+ --version-name <name> App versionName, required with --mapping
92
+ --version-code <code> App versionCode (digits only), required with
93
+ --mapping
87
94
  --project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
88
95
  --endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or
89
96
  https://ingest.observerkit.com`;
@@ -94,6 +101,9 @@ async function main() {
94
101
  "packager-map": { type: "string" },
95
102
  bundle: { type: "string" },
96
103
  dir: { type: "string" },
104
+ mapping: { type: "string" },
105
+ "version-name": { type: "string" },
106
+ "version-code": { type: "string" },
97
107
  "project-key": { type: "string" },
98
108
  endpoint: { type: "string" }
99
109
  }
@@ -106,7 +116,7 @@ async function main() {
106
116
  process.exit(1);
107
117
  }
108
118
  const endpoint = _nullishCoalesce(_nullishCoalesce(values.endpoint, () => ( process.env["OBSERVERKIT_ENDPOINT"])), () => ( "https://ingest.observerkit.com"));
109
- if (!values.map && !values.dir) {
119
+ if (!values.map && !values.dir && !values.mapping) {
110
120
  console.error(USAGE);
111
121
  process.exit(1);
112
122
  }
@@ -115,6 +125,34 @@ async function main() {
115
125
  "[ObserverKit] --packager-map and --bundle are ignored when --dir is set"
116
126
  );
117
127
  }
128
+ if (values.mapping) {
129
+ if (values.map || values.dir) {
130
+ console.error("[ObserverKit] --mapping cannot be combined with --map or --dir");
131
+ process.exit(1);
132
+ }
133
+ const versionName = values["version-name"];
134
+ const versionCode = values["version-code"];
135
+ if (!versionName || !versionCode) {
136
+ console.error("[ObserverKit] --mapping requires --version-name and --version-code");
137
+ process.exit(1);
138
+ }
139
+ if (!/^\d+$/.test(versionCode)) {
140
+ console.error(
141
+ `[ObserverKit] --version-code must be a string of digits, got "${versionCode}"`
142
+ );
143
+ process.exit(1);
144
+ }
145
+ await _chunkDFKRMIQJcjs.uploadProguardMapping.call(void 0, {
146
+ projectKey,
147
+ endpoint,
148
+ mappingPath: _path.resolve.call(void 0, values.mapping),
149
+ versionName,
150
+ versionCode,
151
+ pluginVersion: PLUGIN_VERSION,
152
+ pluginName: "metro"
153
+ });
154
+ return;
155
+ }
118
156
  if (values.map) {
119
157
  if (!values.map.endsWith(".map")) {
120
158
  console.error(`[ObserverKit] --map must point at a .map file, got "${values.map}"`);
@@ -128,7 +166,7 @@ async function main() {
128
166
  packagerMap ? _path.resolve.call(void 0, packagerMap) : void 0,
129
167
  bundle ? _path.resolve.call(void 0, bundle) : void 0
130
168
  );
131
- await _chunk6RJOM2COcjs.uploadSourceMaps.call(void 0, {
169
+ await _chunkDFKRMIQJcjs.uploadSourceMaps.call(void 0, {
132
170
  projectKey,
133
171
  directory: _path.dirname.call(void 0, mapPath),
134
172
  endpoint,
@@ -140,7 +178,7 @@ async function main() {
140
178
  return;
141
179
  }
142
180
  if (values.dir) {
143
- await _chunk6RJOM2COcjs.uploadSourceMaps.call(void 0, {
181
+ await _chunkDFKRMIQJcjs.uploadSourceMaps.call(void 0, {
144
182
  projectKey,
145
183
  directory: _path.resolve.call(void 0, values.dir),
146
184
  endpoint,
package/dist/cli.js CHANGED
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  isJsonObject,
4
+ uploadProguardMapping,
4
5
  uploadSourceMaps
5
- } from "./chunk-DFCKYXBG.js";
6
+ } from "./chunk-3IIVAXY2.js";
6
7
 
7
8
  // src/cli.ts
8
9
  import { parseArgs } from "util";
@@ -70,10 +71,11 @@ async function repairDebugId(mapPath, packagerMapPath, bundlePath) {
70
71
  }
71
72
 
72
73
  // src/cli.ts
73
- var PLUGIN_VERSION = "0.2.0";
74
+ var PLUGIN_VERSION = "0.2.1";
74
75
  var USAGE = `Usage:
75
76
  observerkit-upload --map <path> [--packager-map <path>] [--bundle <path>]
76
77
  observerkit-upload --dir <path>
78
+ observerkit-upload --mapping <path> --version-name <name> --version-code <code>
77
79
 
78
80
  Options:
79
81
  --map <path> Final source map to upload (repairs debugId first)
@@ -83,6 +85,11 @@ Options:
83
85
  packager map is available (Hermes iOS builds)
84
86
  --dir <path> Upload every debug-id source map in a directory
85
87
  (e.g. an expo export output)
88
+ --mapping <path> R8/ProGuard mapping.txt to upload, keyed by
89
+ --version-name and --version-code
90
+ --version-name <name> App versionName, required with --mapping
91
+ --version-code <code> App versionCode (digits only), required with
92
+ --mapping
86
93
  --project-key <key> Defaults to $OBSERVERKIT_PROJECT_KEY
87
94
  --endpoint <url> Defaults to $OBSERVERKIT_ENDPOINT or
88
95
  https://ingest.observerkit.com`;
@@ -93,6 +100,9 @@ async function main() {
93
100
  "packager-map": { type: "string" },
94
101
  bundle: { type: "string" },
95
102
  dir: { type: "string" },
103
+ mapping: { type: "string" },
104
+ "version-name": { type: "string" },
105
+ "version-code": { type: "string" },
96
106
  "project-key": { type: "string" },
97
107
  endpoint: { type: "string" }
98
108
  }
@@ -105,7 +115,7 @@ async function main() {
105
115
  process.exit(1);
106
116
  }
107
117
  const endpoint = values.endpoint ?? process.env["OBSERVERKIT_ENDPOINT"] ?? "https://ingest.observerkit.com";
108
- if (!values.map && !values.dir) {
118
+ if (!values.map && !values.dir && !values.mapping) {
109
119
  console.error(USAGE);
110
120
  process.exit(1);
111
121
  }
@@ -114,6 +124,34 @@ async function main() {
114
124
  "[ObserverKit] --packager-map and --bundle are ignored when --dir is set"
115
125
  );
116
126
  }
127
+ if (values.mapping) {
128
+ if (values.map || values.dir) {
129
+ console.error("[ObserverKit] --mapping cannot be combined with --map or --dir");
130
+ process.exit(1);
131
+ }
132
+ const versionName = values["version-name"];
133
+ const versionCode = values["version-code"];
134
+ if (!versionName || !versionCode) {
135
+ console.error("[ObserverKit] --mapping requires --version-name and --version-code");
136
+ process.exit(1);
137
+ }
138
+ if (!/^\d+$/.test(versionCode)) {
139
+ console.error(
140
+ `[ObserverKit] --version-code must be a string of digits, got "${versionCode}"`
141
+ );
142
+ process.exit(1);
143
+ }
144
+ await uploadProguardMapping({
145
+ projectKey,
146
+ endpoint,
147
+ mappingPath: resolve(values.mapping),
148
+ versionName,
149
+ versionCode,
150
+ pluginVersion: PLUGIN_VERSION,
151
+ pluginName: "metro"
152
+ });
153
+ return;
154
+ }
117
155
  if (values.map) {
118
156
  if (!values.map.endsWith(".map")) {
119
157
  console.error(`[ObserverKit] --map must point at a .map file, got "${values.map}"`);
package/dist/expo.cjs CHANGED
@@ -5,6 +5,9 @@ var _configplugins = require('@expo/config-plugins');
5
5
 
6
6
  // src/native-scripts.ts
7
7
  var UPLOAD_MARKER = "observerkit-upload";
8
+ var MAPPING_UPLOAD_MARKER = "observerkit-mapping-upload";
9
+ var UPLOAD_MARKER_COMMENT = `// ${UPLOAD_MARKER} (added`;
10
+ var MAPPING_UPLOAD_MARKER_COMMENT = `// ${MAPPING_UPLOAD_MARKER} (added`;
8
11
  function escapeForDoubleQuoted(value) {
9
12
  return value.replace(/[\\"]/g, "\\$&");
10
13
  }
@@ -25,11 +28,14 @@ function patchIosShellScript(script, props) {
25
28
  const patched = `${exportLine}${inner}${uploadBlock}`;
26
29
  return hasQuotes ? `"${patched}"` : patched;
27
30
  }
28
- function buildGradleSnippet(props) {
31
+ function gradleEnvLines(props) {
29
32
  const envLine = props.projectKey ? `
30
33
  environment "OBSERVERKIT_PROJECT_KEY", "${escapeForDoubleQuoted(props.projectKey)}"` : "";
31
34
  const endpointLine = props.endpoint ? `
32
35
  environment "OBSERVERKIT_ENDPOINT", "${escapeForDoubleQuoted(props.endpoint)}"` : "";
36
+ return `${envLine}${endpointLine}`;
37
+ }
38
+ function buildGradleSnippet(props) {
33
39
  return `
34
40
  // ${UPLOAD_MARKER} (added by @observerkit/metro/expo; do not edit)
35
41
  tasks.configureEach { task ->
@@ -44,7 +50,7 @@ tasks.configureEach { task ->
44
50
  def packagerMapFile = file("$buildDir/intermediates/sourcemaps/react/" + variant + "/index.android.bundle.packager.map")
45
51
  if (mapFile.exists()) {
46
52
  def result = exec {
47
- workingDir rootProject.projectDir.parentFile${envLine}${endpointLine}
53
+ workingDir rootProject.projectDir.parentFile${gradleEnvLines(props)}
48
54
  commandLine "npx", "observerkit-upload", "--map", mapFile.absolutePath, "--packager-map", packagerMapFile.absolutePath
49
55
  ignoreExitValue true
50
56
  }
@@ -59,6 +65,38 @@ tasks.configureEach { task ->
59
65
  }
60
66
  `;
61
67
  }
68
+ function mappingGradleSnippet(props) {
69
+ return `
70
+ // ${MAPPING_UPLOAD_MARKER} (added by @observerkit/metro/expo; do not edit)
71
+ tasks.configureEach { task ->
72
+ def matcher = task.name =~ /^minify(\\w*)ReleaseWithR8$/
73
+ if (matcher.matches()) {
74
+ task.doLast {
75
+ def flavor = matcher.group(1)
76
+ def variant = flavor.isEmpty()
77
+ ? "release"
78
+ : flavor.substring(0, 1).toLowerCase() + flavor.substring(1) + "Release"
79
+ def mappingFile = file("$buildDir/outputs/mapping/" + variant + "/mapping.txt")
80
+ def androidExt = project.extensions.getByName("android")
81
+ def versionName = androidExt.defaultConfig.versionName
82
+ def versionCode = androidExt.defaultConfig.versionCode
83
+ if (mappingFile.exists() && versionName != null && versionCode != null) {
84
+ def result = exec {
85
+ workingDir rootProject.projectDir.parentFile${gradleEnvLines(props)}
86
+ commandLine "npx", "observerkit-upload", "--mapping", mappingFile.absolutePath, "--version-name", versionName.toString(), "--version-code", versionCode.toString()
87
+ ignoreExitValue true
88
+ }
89
+ if (result.exitValue != 0) {
90
+ logger.warn("[ObserverKit] R8 mapping upload failed")
91
+ }
92
+ } else {
93
+ logger.warn("[ObserverKit] No R8 mapping found at " + mappingFile + ", skipping upload")
94
+ }
95
+ }
96
+ }
97
+ }
98
+ `;
99
+ }
62
100
 
63
101
  // src/expo.ts
64
102
  var BUNDLE_PHASE_NAME = "Bundle React Native code and images";
@@ -75,9 +113,12 @@ var withObserverkitIos = (config, props) => _configplugins.withXcodeProject.call
75
113
  return c;
76
114
  });
77
115
  var withObserverkitAndroid = (config, props) => _configplugins.withAppBuildGradle.call(void 0, config, (c) => {
78
- if (!c.modResults.contents.includes(UPLOAD_MARKER)) {
116
+ if (!c.modResults.contents.includes(UPLOAD_MARKER_COMMENT)) {
79
117
  c.modResults.contents = c.modResults.contents + buildGradleSnippet(props);
80
118
  }
119
+ if (!c.modResults.contents.includes(MAPPING_UPLOAD_MARKER_COMMENT)) {
120
+ c.modResults.contents = c.modResults.contents + mappingGradleSnippet(props);
121
+ }
81
122
  return c;
82
123
  });
83
124
  var withObserverkitExpo = (config, props) => {
package/dist/expo.js CHANGED
@@ -3,6 +3,9 @@ import { withAppBuildGradle, withXcodeProject } from "@expo/config-plugins";
3
3
 
4
4
  // src/native-scripts.ts
5
5
  var UPLOAD_MARKER = "observerkit-upload";
6
+ var MAPPING_UPLOAD_MARKER = "observerkit-mapping-upload";
7
+ var UPLOAD_MARKER_COMMENT = `// ${UPLOAD_MARKER} (added`;
8
+ var MAPPING_UPLOAD_MARKER_COMMENT = `// ${MAPPING_UPLOAD_MARKER} (added`;
6
9
  function escapeForDoubleQuoted(value) {
7
10
  return value.replace(/[\\"]/g, "\\$&");
8
11
  }
@@ -23,11 +26,14 @@ function patchIosShellScript(script, props) {
23
26
  const patched = `${exportLine}${inner}${uploadBlock}`;
24
27
  return hasQuotes ? `"${patched}"` : patched;
25
28
  }
26
- function buildGradleSnippet(props) {
29
+ function gradleEnvLines(props) {
27
30
  const envLine = props.projectKey ? `
28
31
  environment "OBSERVERKIT_PROJECT_KEY", "${escapeForDoubleQuoted(props.projectKey)}"` : "";
29
32
  const endpointLine = props.endpoint ? `
30
33
  environment "OBSERVERKIT_ENDPOINT", "${escapeForDoubleQuoted(props.endpoint)}"` : "";
34
+ return `${envLine}${endpointLine}`;
35
+ }
36
+ function buildGradleSnippet(props) {
31
37
  return `
32
38
  // ${UPLOAD_MARKER} (added by @observerkit/metro/expo; do not edit)
33
39
  tasks.configureEach { task ->
@@ -42,7 +48,7 @@ tasks.configureEach { task ->
42
48
  def packagerMapFile = file("$buildDir/intermediates/sourcemaps/react/" + variant + "/index.android.bundle.packager.map")
43
49
  if (mapFile.exists()) {
44
50
  def result = exec {
45
- workingDir rootProject.projectDir.parentFile${envLine}${endpointLine}
51
+ workingDir rootProject.projectDir.parentFile${gradleEnvLines(props)}
46
52
  commandLine "npx", "observerkit-upload", "--map", mapFile.absolutePath, "--packager-map", packagerMapFile.absolutePath
47
53
  ignoreExitValue true
48
54
  }
@@ -57,6 +63,38 @@ tasks.configureEach { task ->
57
63
  }
58
64
  `;
59
65
  }
66
+ function mappingGradleSnippet(props) {
67
+ return `
68
+ // ${MAPPING_UPLOAD_MARKER} (added by @observerkit/metro/expo; do not edit)
69
+ tasks.configureEach { task ->
70
+ def matcher = task.name =~ /^minify(\\w*)ReleaseWithR8$/
71
+ if (matcher.matches()) {
72
+ task.doLast {
73
+ def flavor = matcher.group(1)
74
+ def variant = flavor.isEmpty()
75
+ ? "release"
76
+ : flavor.substring(0, 1).toLowerCase() + flavor.substring(1) + "Release"
77
+ def mappingFile = file("$buildDir/outputs/mapping/" + variant + "/mapping.txt")
78
+ def androidExt = project.extensions.getByName("android")
79
+ def versionName = androidExt.defaultConfig.versionName
80
+ def versionCode = androidExt.defaultConfig.versionCode
81
+ if (mappingFile.exists() && versionName != null && versionCode != null) {
82
+ def result = exec {
83
+ workingDir rootProject.projectDir.parentFile${gradleEnvLines(props)}
84
+ commandLine "npx", "observerkit-upload", "--mapping", mappingFile.absolutePath, "--version-name", versionName.toString(), "--version-code", versionCode.toString()
85
+ ignoreExitValue true
86
+ }
87
+ if (result.exitValue != 0) {
88
+ logger.warn("[ObserverKit] R8 mapping upload failed")
89
+ }
90
+ } else {
91
+ logger.warn("[ObserverKit] No R8 mapping found at " + mappingFile + ", skipping upload")
92
+ }
93
+ }
94
+ }
95
+ }
96
+ `;
97
+ }
60
98
 
61
99
  // src/expo.ts
62
100
  var BUNDLE_PHASE_NAME = "Bundle React Native code and images";
@@ -73,9 +111,12 @@ var withObserverkitIos = (config, props) => withXcodeProject(config, (c) => {
73
111
  return c;
74
112
  });
75
113
  var withObserverkitAndroid = (config, props) => withAppBuildGradle(config, (c) => {
76
- if (!c.modResults.contents.includes(UPLOAD_MARKER)) {
114
+ if (!c.modResults.contents.includes(UPLOAD_MARKER_COMMENT)) {
77
115
  c.modResults.contents = c.modResults.contents + buildGradleSnippet(props);
78
116
  }
117
+ if (!c.modResults.contents.includes(MAPPING_UPLOAD_MARKER_COMMENT)) {
118
+ c.modResults.contents = c.modResults.contents + mappingGradleSnippet(props);
119
+ }
79
120
  return c;
80
121
  });
81
122
  var withObserverkitExpo = (config, props) => {
package/dist/index.cjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
 
4
4
 
5
- var _chunk6RJOM2COcjs = require('./chunk-6RJOM2CO.cjs');
5
+ var _chunkDFKRMIQJcjs = require('./chunk-DFKRMIQJ.cjs');
6
6
 
7
7
 
8
8
  var _chunkAUOVSTQXcjs = require('./chunk-AUOVSTQX.cjs');
@@ -42,15 +42,15 @@ function addDebugIdToMap(map, debugId) {
42
42
  } catch (e) {
43
43
  return map;
44
44
  }
45
- if (!_chunk6RJOM2COcjs.isJsonObject.call(void 0, parsed)) return map;
45
+ if (!_chunkDFKRMIQJcjs.isJsonObject.call(void 0, parsed)) return map;
46
46
  return JSON.stringify({ ...parsed, debugId });
47
47
  }
48
48
  function unwrapModuleExport(mod, exportName) {
49
49
  if (exportName) {
50
- const named = _chunk6RJOM2COcjs.isJsonObject.call(void 0, mod) ? mod[exportName] : void 0;
50
+ const named = _chunkDFKRMIQJcjs.isJsonObject.call(void 0, mod) ? mod[exportName] : void 0;
51
51
  if (typeof named === "function") return named;
52
52
  }
53
- const defaultExport = _chunk6RJOM2COcjs.isJsonObject.call(void 0, mod) ? mod["default"] : void 0;
53
+ const defaultExport = _chunkDFKRMIQJcjs.isJsonObject.call(void 0, mod) ? mod["default"] : void 0;
54
54
  if (typeof defaultExport === "function") return defaultExport;
55
55
  if (typeof mod === "function") return mod;
56
56
  throw new Error(
@@ -103,12 +103,12 @@ function createObserverkitSerializer(wrapped) {
103
103
  if (options.dev) {
104
104
  return serializer(entryPoint, preModules, graph, options);
105
105
  }
106
- const { snippet } = _chunk6RJOM2COcjs.buildStackKeyedDebugIdSnippet.call(void 0, DEBUG_ID_PLACEHOLDER);
106
+ const { snippet } = _chunkDFKRMIQJcjs.buildStackKeyedDebugIdSnippet.call(void 0, DEBUG_ID_PLACEHOLDER);
107
107
  const withDebugId = injectDebugIdModule(preModules, createDebugIdModule(snippet));
108
108
  const result = await serializer(entryPoint, withDebugId, graph, options);
109
109
  const code = typeof result === "string" ? result : result.code;
110
110
  const map = typeof result === "string" ? null : result.map;
111
- const debugId = _chunk6RJOM2COcjs.generateDebugId.call(void 0, code);
111
+ const debugId = _chunkDFKRMIQJcjs.generateDebugId.call(void 0, code);
112
112
  const finalCode = `${code.split(DEBUG_ID_PLACEHOLDER).join(debugId)}
113
113
  //# debugId=${debugId}`;
114
114
  if (map === null) return finalCode;
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  buildStackKeyedDebugIdSnippet,
3
3
  generateDebugId,
4
4
  isJsonObject
5
- } from "./chunk-DFCKYXBG.js";
5
+ } from "./chunk-3IIVAXY2.js";
6
6
 
7
7
  // src/serializer.ts
8
8
  import { createRequire } from "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@observerkit/metro",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Metro plugin for ObserverKit: injects debug IDs and uploads React Native source maps",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -45,7 +45,7 @@
45
45
  "tsup": "^8.3.5",
46
46
  "typescript": "5.9.2",
47
47
  "vitest": "^3.2.4",
48
- "@repo/plugin-core": "0.0.0"
48
+ "@repo/plugin-core": "0.0.1"
49
49
  },
50
50
  "dependencies": {
51
51
  "uuid": "^13.0.0"