@grafana/faro-react-native 1.1.0 → 1.2.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.
Files changed (40) hide show
  1. package/README.md +72 -0
  2. package/android/build.gradle +145 -0
  3. package/dist/cjs/config/getRNInstrumentations.js +3 -1
  4. package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
  5. package/dist/cjs/config/makeRNConfig.js +7 -4
  6. package/dist/cjs/config/makeRNConfig.js.map +1 -1
  7. package/dist/cjs/config/types.js.map +1 -1
  8. package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
  9. package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
  10. package/dist/cjs/instrumentations/errors/index.js +3 -2
  11. package/dist/cjs/instrumentations/errors/index.js.map +1 -1
  12. package/dist/cjs/instrumentations/errors/stackTraceParser.js +98 -37
  13. package/dist/cjs/instrumentations/errors/stackTraceParser.js.map +1 -1
  14. package/dist/esm/config/getRNInstrumentations.js +3 -1
  15. package/dist/esm/config/getRNInstrumentations.js.map +1 -1
  16. package/dist/esm/config/makeRNConfig.js +7 -4
  17. package/dist/esm/config/makeRNConfig.js.map +1 -1
  18. package/dist/esm/config/types.js.map +1 -1
  19. package/dist/esm/generated/faroRNPackageMeta.js +1 -1
  20. package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
  21. package/dist/esm/instrumentations/errors/index.js +3 -2
  22. package/dist/esm/instrumentations/errors/index.js.map +1 -1
  23. package/dist/esm/instrumentations/errors/stackTraceParser.js +97 -37
  24. package/dist/esm/instrumentations/errors/stackTraceParser.js.map +1 -1
  25. package/dist/types/config/types.d.ts +5 -0
  26. package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
  27. package/dist/types/instrumentations/errors/index.d.ts +5 -0
  28. package/dist/types/instrumentations/errors/stackTraceParser.d.ts +19 -3
  29. package/ios/faro-upload-composed-source-map.sh +87 -0
  30. package/package.json +7 -10
  31. package/react-native.config.js +9 -1
  32. package/src/config/getRNInstrumentations.ts +5 -1
  33. package/src/config/makeRNConfig.test.ts +11 -0
  34. package/src/config/makeRNConfig.ts +7 -4
  35. package/src/config/types.ts +5 -0
  36. package/src/generated/faroRNPackageMeta.ts +1 -1
  37. package/src/initialize.test.ts +25 -0
  38. package/src/instrumentations/errors/index.ts +25 -8
  39. package/src/instrumentations/errors/stackTraceParser.test.ts +65 -0
  40. package/src/instrumentations/errors/stackTraceParser.ts +121 -37
@@ -21,67 +21,113 @@ const REACT_NATIVE_RELEASE_REGEX = /^(.+?)@(\d+):(\d+)$/;
21
21
  const REACT_NATIVE_ANONYMOUS_REGEX = /^\s*at\s+anonymous\s+\((.+?):(\d+):(\d+)\)$/;
22
22
  const METRO_BUNDLER_REGEX = /^\s*at\s+(?:Object\.)?(.+?)\s+\((.+?):(\d+):(\d+)\)$/;
23
23
  /**
24
- * Parse a single stack trace line into a structured frame
24
+ * Hermes (Android release) often reports the bundle segment as `address at index.android.bundle`
25
+ * inside parentheses. The collector hashes `frame.filename` for uploaded maps; it must match the
26
+ * source map `file` field (e.g. `index.android.bundle`), not the verbose Hermes label.
25
27
  */
26
- export function parseStackTraceLine(line) {
28
+ export function normalizeHermesStackFilename(filename) {
29
+ var _a, _b;
30
+ const trimmed = filename.trim();
31
+ const m = /^address\s+at\s+(.+)$/i.exec(trimmed);
32
+ return (_b = (_a = m === null || m === void 0 ? void 0 : m[1]) === null || _a === void 0 ? void 0 : _a.trim()) !== null && _b !== void 0 ? _b : filename;
33
+ }
34
+ /**
35
+ * iOS Simulator (and Android) stacks often carry full filesystem paths such as
36
+ * `…/Foo.app/main.jsbundle` while uploaded maps are keyed by the bundle basename (`main.jsbundle`)
37
+ * that matches Metro’s source map `file` field — same as ingest’s bundle lookup hashes.
38
+ */
39
+ function collapseBundledArtifactPath(filename, releaseBasename) {
40
+ const key = releaseBasename.trim();
41
+ if (!key || filename === key) {
42
+ return filename;
43
+ }
44
+ const suffix = `/${key}`;
45
+ return filename.endsWith(suffix) ? key : filename;
46
+ }
47
+ const UNKNOWN_RELEASE_FILENAME = '<unknown>';
48
+ /**
49
+ * Parse a single stack trace line into a structured frame and record how it was parsed.
50
+ */
51
+ function parseStackTraceLineDetailed(line) {
27
52
  if (!line || typeof line !== 'string') {
28
- return null;
53
+ return { frame: null, kind: 'unparsed' };
29
54
  }
30
55
  const trimmedLine = line.trim();
31
56
  if (!trimmedLine) {
32
- return null;
57
+ return { frame: null, kind: 'unparsed' };
33
58
  }
34
59
  // Try standard React Native format: at functionName (file.js:123:45)
35
60
  let match = trimmedLine.match(REACT_NATIVE_STACK_REGEX);
36
61
  if (match && match[2] && match[3] && match[4]) {
37
62
  return {
38
- function: match[1] || '<anonymous>',
39
- filename: match[2],
40
- lineno: parseInt(match[3], 10),
41
- colno: parseInt(match[4], 10),
63
+ kind: 'react_native_paren',
64
+ frame: {
65
+ function: match[1] || '<anonymous>',
66
+ filename: normalizeHermesStackFilename(match[2]),
67
+ lineno: parseInt(match[3], 10),
68
+ colno: parseInt(match[4], 10),
69
+ },
42
70
  };
43
71
  }
44
72
  // Try Metro bundler format: at Object.functionName (/path/to/file.js:123:456)
45
73
  match = trimmedLine.match(METRO_BUNDLER_REGEX);
46
74
  if (match && match[2] && match[3] && match[4]) {
47
75
  return {
48
- function: match[1] || '<anonymous>',
49
- filename: match[2],
50
- lineno: parseInt(match[3], 10),
51
- colno: parseInt(match[4], 10),
76
+ kind: 'metro_paren',
77
+ frame: {
78
+ function: match[1] || '<anonymous>',
79
+ filename: normalizeHermesStackFilename(match[2]),
80
+ lineno: parseInt(match[3], 10),
81
+ colno: parseInt(match[4], 10),
82
+ },
52
83
  };
53
84
  }
54
85
  // Try anonymous format: at anonymous (file.js:123:45)
55
86
  match = trimmedLine.match(REACT_NATIVE_ANONYMOUS_REGEX);
56
87
  if (match && match[1] && match[2] && match[3]) {
57
88
  return {
58
- function: '<anonymous>',
59
- filename: match[1],
60
- lineno: parseInt(match[2], 10),
61
- colno: parseInt(match[3], 10),
89
+ kind: 'anonymous_paren',
90
+ frame: {
91
+ function: '<anonymous>',
92
+ filename: normalizeHermesStackFilename(match[1]),
93
+ lineno: parseInt(match[2], 10),
94
+ colno: parseInt(match[3], 10),
95
+ },
62
96
  };
63
97
  }
64
98
  // Try native format: at functionName (native)
65
99
  match = trimmedLine.match(REACT_NATIVE_NATIVE_REGEX);
66
100
  if (match) {
67
101
  return {
68
- function: match[1] || '<native>',
69
- filename: '<native>',
70
- isNative: true,
102
+ kind: 'native',
103
+ frame: {
104
+ function: match[1] || '<native>',
105
+ filename: '<native>',
106
+ isNative: true,
107
+ },
71
108
  };
72
109
  }
73
110
  // Try release/minified format: functionName@123:456
74
111
  match = trimmedLine.match(REACT_NATIVE_RELEASE_REGEX);
75
112
  if (match && match[2] && match[3]) {
76
113
  return {
77
- function: match[1] || '<anonymous>',
78
- filename: '<unknown>',
79
- lineno: parseInt(match[2], 10),
80
- colno: parseInt(match[3], 10),
114
+ kind: 'release_func_at',
115
+ frame: {
116
+ function: match[1] || '<anonymous>',
117
+ filename: UNKNOWN_RELEASE_FILENAME,
118
+ lineno: parseInt(match[2], 10),
119
+ colno: parseInt(match[3], 10),
120
+ releaseLine: true,
121
+ },
81
122
  };
82
123
  }
83
- // Couldn't parse this line
84
- return null;
124
+ return { frame: null, kind: 'unparsed' };
125
+ }
126
+ /**
127
+ * Parse a single stack trace line into a structured frame
128
+ */
129
+ export function parseStackTraceLine(line) {
130
+ return parseStackTraceLineDetailed(line).frame;
85
131
  }
86
132
  /**
87
133
  * Parse a full stack trace string into an array of structured frames
@@ -93,7 +139,7 @@ export function parseStackTrace(stackTrace) {
93
139
  const lines = stackTrace.split('\n');
94
140
  const frames = [];
95
141
  for (const line of lines) {
96
- const frame = parseStackTraceLine(line);
142
+ const { frame } = parseStackTraceLineDetailed(line);
97
143
  if (frame) {
98
144
  frames.push(frame);
99
145
  }
@@ -103,24 +149,38 @@ export function parseStackTrace(stackTrace) {
103
149
  /**
104
150
  * Convert parsed stack frames to Faro's StackFrame format
105
151
  */
106
- export function toFaroStackFrames(parsedFrames) {
107
- return parsedFrames.map((frame, _index) => ({
108
- filename: frame.filename || '<unknown>',
109
- function: frame.function || '<anonymous>',
110
- lineno: frame.lineno,
111
- colno: frame.colno,
112
- }));
152
+ export function toFaroStackFrames(parsedFrames, options) {
153
+ var _a;
154
+ const releaseName = (_a = options === null || options === void 0 ? void 0 : options.releaseBundleFilename) !== null && _a !== void 0 ? _a : 'bundle.js';
155
+ const keyedBasename = typeof (options === null || options === void 0 ? void 0 : options.releaseBundleFilename) === 'string' && options.releaseBundleFilename.trim() !== ''
156
+ ? options.releaseBundleFilename.trim()
157
+ : null;
158
+ return parsedFrames.map((frame, _index) => {
159
+ let filename = normalizeHermesStackFilename(frame.filename || '<unknown>');
160
+ if (frame.releaseLine) {
161
+ filename = releaseName;
162
+ }
163
+ else if (keyedBasename) {
164
+ filename = collapseBundledArtifactPath(filename, keyedBasename);
165
+ }
166
+ return {
167
+ filename,
168
+ function: frame.function || '<anonymous>',
169
+ lineno: frame.lineno,
170
+ colno: frame.colno,
171
+ };
172
+ });
113
173
  }
114
174
  /**
115
175
  * Extract and parse stack frames from an Error object
116
176
  */
117
- export function getStackFramesFromError(error) {
177
+ export function getStackFramesFromError(error, options) {
118
178
  if (!error || !error.stack) {
119
179
  return [];
120
180
  }
121
181
  try {
122
182
  const parsedFrames = parseStackTrace(error.stack);
123
- return toFaroStackFrames(parsedFrames);
183
+ return toFaroStackFrames(parsedFrames, options);
124
184
  }
125
185
  catch (_e) {
126
186
  // If parsing fails, return empty array
@@ -141,8 +201,8 @@ export function getPlatformErrorContext() {
141
201
  /**
142
202
  * Enhance error with React Native specific information
143
203
  */
144
- export function enhanceErrorWithContext(error, additionalContext) {
145
- const stackFrames = getStackFramesFromError(error);
204
+ export function enhanceErrorWithContext(error, additionalContext, stackParseOptions) {
205
+ const stackFrames = getStackFramesFromError(error, stackParseOptions);
146
206
  const platformContext = getPlatformErrorContext();
147
207
  return {
148
208
  error,
@@ -1 +1 @@
1
- {"version":3,"file":"stackTraceParser.js","sourceRoot":"","sources":["../../../../src/instrumentations/errors/stackTraceParser.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from 'react-native';\n\nimport type { ExceptionStackFrame as StackFrame } from '@grafana/faro-core';\n\n// Declare global Hermes runtime\ndeclare const HermesInternal: object | undefined;\n\n/**\n * Parse React Native stack traces into structured stack frames\n *\n * React Native stack traces have different formats depending on platform and environment:\n *\n * iOS/Android (Dev):\n * at functionName (file.js:123:45)\n * at anonymous (native)\n *\n * iOS/Android (Release):\n * functionName@123:456\n * @[native code]\n *\n * Metro bundler:\n * at Object.functionName (/path/to/file.js:123:456)\n */\n\nconst REACT_NATIVE_STACK_REGEX = /^\\s*at\\s+(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\nconst REACT_NATIVE_NATIVE_REGEX = /^\\s*at\\s+(.+?)\\s+\\(native\\)$/;\nconst REACT_NATIVE_RELEASE_REGEX = /^(.+?)@(\\d+):(\\d+)$/;\nconst REACT_NATIVE_ANONYMOUS_REGEX = /^\\s*at\\s+anonymous\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\nconst METRO_BUNDLER_REGEX = /^\\s*at\\s+(?:Object\\.)?(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\n\nexport interface ParsedStackFrame {\n function?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n isNative?: boolean;\n}\n\n/**\n * Parse a single stack trace line into a structured frame\n */\nexport function parseStackTraceLine(line: string): ParsedStackFrame | null {\n if (!line || typeof line !== 'string') {\n return null;\n }\n\n const trimmedLine = line.trim();\n if (!trimmedLine) {\n return null;\n }\n\n // Try standard React Native format: at functionName (file.js:123:45)\n let match = trimmedLine.match(REACT_NATIVE_STACK_REGEX);\n if (match && match[2] && match[3] && match[4]) {\n return {\n function: match[1] || '<anonymous>',\n filename: match[2],\n lineno: parseInt(match[3], 10),\n colno: parseInt(match[4], 10),\n };\n }\n\n // Try Metro bundler format: at Object.functionName (/path/to/file.js:123:456)\n match = trimmedLine.match(METRO_BUNDLER_REGEX);\n if (match && match[2] && match[3] && match[4]) {\n return {\n function: match[1] || '<anonymous>',\n filename: match[2],\n lineno: parseInt(match[3], 10),\n colno: parseInt(match[4], 10),\n };\n }\n\n // Try anonymous format: at anonymous (file.js:123:45)\n match = trimmedLine.match(REACT_NATIVE_ANONYMOUS_REGEX);\n if (match && match[1] && match[2] && match[3]) {\n return {\n function: '<anonymous>',\n filename: match[1],\n lineno: parseInt(match[2], 10),\n colno: parseInt(match[3], 10),\n };\n }\n\n // Try native format: at functionName (native)\n match = trimmedLine.match(REACT_NATIVE_NATIVE_REGEX);\n if (match) {\n return {\n function: match[1] || '<native>',\n filename: '<native>',\n isNative: true,\n };\n }\n\n // Try release/minified format: functionName@123:456\n match = trimmedLine.match(REACT_NATIVE_RELEASE_REGEX);\n if (match && match[2] && match[3]) {\n return {\n function: match[1] || '<anonymous>',\n filename: '<unknown>',\n lineno: parseInt(match[2], 10),\n colno: parseInt(match[3], 10),\n };\n }\n\n // Couldn't parse this line\n return null;\n}\n\n/**\n * Parse a full stack trace string into an array of structured frames\n */\nexport function parseStackTrace(stackTrace: string): ParsedStackFrame[] {\n if (!stackTrace || typeof stackTrace !== 'string') {\n return [];\n }\n\n const lines = stackTrace.split('\\n');\n const frames: ParsedStackFrame[] = [];\n\n for (const line of lines) {\n const frame = parseStackTraceLine(line);\n if (frame) {\n frames.push(frame);\n }\n }\n\n return frames;\n}\n\n/**\n * Convert parsed stack frames to Faro's StackFrame format\n */\nexport function toFaroStackFrames(parsedFrames: ParsedStackFrame[]): StackFrame[] {\n return parsedFrames.map((frame, _index) => ({\n filename: frame.filename || '<unknown>',\n function: frame.function || '<anonymous>',\n lineno: frame.lineno,\n colno: frame.colno,\n }));\n}\n\n/**\n * Extract and parse stack frames from an Error object\n */\nexport function getStackFramesFromError(error: Error): StackFrame[] {\n if (!error || !error.stack) {\n return [];\n }\n\n try {\n const parsedFrames = parseStackTrace(error.stack);\n return toFaroStackFrames(parsedFrames);\n } catch (_e) {\n // If parsing fails, return empty array\n return [];\n }\n}\n\n/**\n * Get platform-specific error context\n */\nexport function getPlatformErrorContext(): Record<string, string> {\n return {\n platform: Platform.OS,\n platformVersion: Platform.Version?.toString() || 'unknown',\n isHermes: typeof HermesInternal !== 'undefined' ? 'true' : 'false',\n };\n}\n\n/**\n * Enhance error with React Native specific information\n */\nexport function enhanceErrorWithContext(\n error: Error,\n additionalContext?: Record<string, string>\n): {\n error: Error;\n stackFrames: StackFrame[];\n context: Record<string, string>;\n} {\n const stackFrames = getStackFramesFromError(error);\n const platformContext = getPlatformErrorContext();\n\n return {\n error,\n stackFrames,\n context: {\n ...platformContext,\n ...additionalContext,\n },\n };\n}\n"]}
1
+ {"version":3,"file":"stackTraceParser.js","sourceRoot":"","sources":["../../../../src/instrumentations/errors/stackTraceParser.ts"],"names":[],"mappings":"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","sourcesContent":["import { Platform } from 'react-native';\n\nimport type { ExceptionStackFrame as StackFrame } from '@grafana/faro-core';\n\n// Declare global Hermes runtime\ndeclare const HermesInternal: object | undefined;\n\n/**\n * Parse React Native stack traces into structured stack frames\n *\n * React Native stack traces have different formats depending on platform and environment:\n *\n * iOS/Android (Dev):\n * at functionName (file.js:123:45)\n * at anonymous (native)\n *\n * iOS/Android (Release):\n * functionName@123:456\n * @[native code]\n *\n * Metro bundler:\n * at Object.functionName (/path/to/file.js:123:456)\n */\n\nconst REACT_NATIVE_STACK_REGEX = /^\\s*at\\s+(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\nconst REACT_NATIVE_NATIVE_REGEX = /^\\s*at\\s+(.+?)\\s+\\(native\\)$/;\nconst REACT_NATIVE_RELEASE_REGEX = /^(.+?)@(\\d+):(\\d+)$/;\nconst REACT_NATIVE_ANONYMOUS_REGEX = /^\\s*at\\s+anonymous\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\nconst METRO_BUNDLER_REGEX = /^\\s*at\\s+(?:Object\\.)?(.+?)\\s+\\((.+?):(\\d+):(\\d+)\\)$/;\n\n/**\n * Hermes (Android release) often reports the bundle segment as `address at index.android.bundle`\n * inside parentheses. The collector hashes `frame.filename` for uploaded maps; it must match the\n * source map `file` field (e.g. `index.android.bundle`), not the verbose Hermes label.\n */\nexport function normalizeHermesStackFilename(filename: string): string {\n const trimmed = filename.trim();\n const m = /^address\\s+at\\s+(.+)$/i.exec(trimmed);\n return m?.[1]?.trim() ?? filename;\n}\n\n/**\n * iOS Simulator (and Android) stacks often carry full filesystem paths such as\n * `…/Foo.app/main.jsbundle` while uploaded maps are keyed by the bundle basename (`main.jsbundle`)\n * that matches Metro’s source map `file` field — same as ingest’s bundle lookup hashes.\n */\nfunction collapseBundledArtifactPath(filename: string, releaseBasename: string): string {\n const key = releaseBasename.trim();\n if (!key || filename === key) {\n return filename;\n }\n const suffix = `/${key}`;\n return filename.endsWith(suffix) ? key : filename;\n}\n\nexport interface ParsedStackFrame {\n function?: string;\n filename?: string;\n lineno?: number;\n colno?: number;\n isNative?: boolean;\n /** Set when the line matched Hermes/minified `func@line:col` (no path). */\n releaseLine?: boolean;\n}\n\n/** Which parser branch handled a stack line (internal). */\ntype ParsedStackLineKind =\n | 'react_native_paren'\n | 'metro_paren'\n | 'anonymous_paren'\n | 'native'\n | 'release_func_at'\n | 'unparsed';\n\nconst UNKNOWN_RELEASE_FILENAME = '<unknown>';\n\nexport interface StackFrameParseOptions {\n /**\n * Hermes / minified RN release lines use `func@line:col` with no file path. Those frames are tagged\n * with `releaseLine` and use this as `filename` (default `bundle.js`). Match the Metro plugin’s\n * source map `file` field (or `sourceMapFile` option there).\n */\n releaseBundleFilename?: string;\n}\n\n/**\n * Parse a single stack trace line into a structured frame and record how it was parsed.\n */\nfunction parseStackTraceLineDetailed(line: string): { frame: ParsedStackFrame | null; kind: ParsedStackLineKind } {\n if (!line || typeof line !== 'string') {\n return { frame: null, kind: 'unparsed' };\n }\n\n const trimmedLine = line.trim();\n if (!trimmedLine) {\n return { frame: null, kind: 'unparsed' };\n }\n\n // Try standard React Native format: at functionName (file.js:123:45)\n let match = trimmedLine.match(REACT_NATIVE_STACK_REGEX);\n if (match && match[2] && match[3] && match[4]) {\n return {\n kind: 'react_native_paren',\n frame: {\n function: match[1] || '<anonymous>',\n filename: normalizeHermesStackFilename(match[2]),\n lineno: parseInt(match[3], 10),\n colno: parseInt(match[4], 10),\n },\n };\n }\n\n // Try Metro bundler format: at Object.functionName (/path/to/file.js:123:456)\n match = trimmedLine.match(METRO_BUNDLER_REGEX);\n if (match && match[2] && match[3] && match[4]) {\n return {\n kind: 'metro_paren',\n frame: {\n function: match[1] || '<anonymous>',\n filename: normalizeHermesStackFilename(match[2]),\n lineno: parseInt(match[3], 10),\n colno: parseInt(match[4], 10),\n },\n };\n }\n\n // Try anonymous format: at anonymous (file.js:123:45)\n match = trimmedLine.match(REACT_NATIVE_ANONYMOUS_REGEX);\n if (match && match[1] && match[2] && match[3]) {\n return {\n kind: 'anonymous_paren',\n frame: {\n function: '<anonymous>',\n filename: normalizeHermesStackFilename(match[1]),\n lineno: parseInt(match[2], 10),\n colno: parseInt(match[3], 10),\n },\n };\n }\n\n // Try native format: at functionName (native)\n match = trimmedLine.match(REACT_NATIVE_NATIVE_REGEX);\n if (match) {\n return {\n kind: 'native',\n frame: {\n function: match[1] || '<native>',\n filename: '<native>',\n isNative: true,\n },\n };\n }\n\n // Try release/minified format: functionName@123:456\n match = trimmedLine.match(REACT_NATIVE_RELEASE_REGEX);\n if (match && match[2] && match[3]) {\n return {\n kind: 'release_func_at',\n frame: {\n function: match[1] || '<anonymous>',\n filename: UNKNOWN_RELEASE_FILENAME,\n lineno: parseInt(match[2], 10),\n colno: parseInt(match[3], 10),\n releaseLine: true,\n },\n };\n }\n\n return { frame: null, kind: 'unparsed' };\n}\n\n/**\n * Parse a single stack trace line into a structured frame\n */\nexport function parseStackTraceLine(line: string): ParsedStackFrame | null {\n return parseStackTraceLineDetailed(line).frame;\n}\n\n/**\n * Parse a full stack trace string into an array of structured frames\n */\nexport function parseStackTrace(stackTrace: string): ParsedStackFrame[] {\n if (!stackTrace || typeof stackTrace !== 'string') {\n return [];\n }\n\n const lines = stackTrace.split('\\n');\n const frames: ParsedStackFrame[] = [];\n\n for (const line of lines) {\n const { frame } = parseStackTraceLineDetailed(line);\n if (frame) {\n frames.push(frame);\n }\n }\n\n return frames;\n}\n\n/**\n * Convert parsed stack frames to Faro's StackFrame format\n */\nexport function toFaroStackFrames(parsedFrames: ParsedStackFrame[], options?: StackFrameParseOptions): StackFrame[] {\n const releaseName = options?.releaseBundleFilename ?? 'bundle.js';\n const keyedBasename =\n typeof options?.releaseBundleFilename === 'string' && options.releaseBundleFilename.trim() !== ''\n ? options.releaseBundleFilename.trim()\n : null;\n\n return parsedFrames.map((frame, _index) => {\n let filename = normalizeHermesStackFilename(frame.filename || '<unknown>');\n if (frame.releaseLine) {\n filename = releaseName;\n } else if (keyedBasename) {\n filename = collapseBundledArtifactPath(filename, keyedBasename);\n }\n return {\n filename,\n function: frame.function || '<anonymous>',\n lineno: frame.lineno,\n colno: frame.colno,\n };\n });\n}\n\n/**\n * Extract and parse stack frames from an Error object\n */\nexport function getStackFramesFromError(error: Error, options?: StackFrameParseOptions): StackFrame[] {\n if (!error || !error.stack) {\n return [];\n }\n\n try {\n const parsedFrames = parseStackTrace(error.stack);\n return toFaroStackFrames(parsedFrames, options);\n } catch (_e) {\n // If parsing fails, return empty array\n return [];\n }\n}\n\n/**\n * Get platform-specific error context\n */\nexport function getPlatformErrorContext(): Record<string, string> {\n return {\n platform: Platform.OS,\n platformVersion: Platform.Version?.toString() || 'unknown',\n isHermes: typeof HermesInternal !== 'undefined' ? 'true' : 'false',\n };\n}\n\n/**\n * Enhance error with React Native specific information\n */\nexport function enhanceErrorWithContext(\n error: Error,\n additionalContext?: Record<string, string>,\n stackParseOptions?: StackFrameParseOptions\n): {\n error: Error;\n stackFrames: StackFrame[];\n context: Record<string, string>;\n} {\n const stackFrames = getStackFramesFromError(error, stackParseOptions);\n const platformContext = getPlatformErrorContext();\n\n return {\n error,\n stackFrames,\n context: {\n ...platformContext,\n ...additionalContext,\n },\n };\n}\n"]}
@@ -62,6 +62,11 @@ type ReactNativeConfigOverrides = {
62
62
  consoleCaptureOptions?: Config['consoleInstrumentation'];
63
63
  enableUserActions?: boolean;
64
64
  userActionsOptions?: Config['userActionsInstrumentation'];
65
+ /**
66
+ * Hermes / minified release stack lines omit a file path. This must match the Metro plugin
67
+ * source map top-level `file` (default `bundle.js`, or your `sourceMapFile` option there).
68
+ */
69
+ releaseBundleFilename?: string;
65
70
  };
66
71
  /**
67
72
  * React Native config: Config (omit replaced props) & overrides.
@@ -2,4 +2,4 @@
2
2
  * NPM package identity for this build. Generated by scripts/sync-faro-rn-package-meta.mjs - do not edit.
3
3
  */
4
4
  export declare const FARO_REACT_NATIVE_NPM_NAME: "@grafana/faro-react-native";
5
- export declare const FARO_REACT_NATIVE_NPM_VERSION: "1.1.0";
5
+ export declare const FARO_REACT_NATIVE_NPM_VERSION: "1.2.0";
@@ -22,6 +22,11 @@ export interface ErrorsInstrumentationOptions {
22
22
  * Older errors are removed when this limit is reached
23
23
  */
24
24
  maxDeduplicationEntries?: number;
25
+ /**
26
+ * Bundle filename for Hermes/minified release lines (`func@line:col`).
27
+ * Must match the Metro plugin source map `file` field (default `bundle.js`).
28
+ */
29
+ releaseBundleFilename?: string;
25
30
  }
26
31
  /**
27
32
  * Errors instrumentation for React Native
@@ -1,10 +1,26 @@
1
1
  import type { ExceptionStackFrame as StackFrame } from '@grafana/faro-core';
2
+ /**
3
+ * Hermes (Android release) often reports the bundle segment as `address at index.android.bundle`
4
+ * inside parentheses. The collector hashes `frame.filename` for uploaded maps; it must match the
5
+ * source map `file` field (e.g. `index.android.bundle`), not the verbose Hermes label.
6
+ */
7
+ export declare function normalizeHermesStackFilename(filename: string): string;
2
8
  export interface ParsedStackFrame {
3
9
  function?: string;
4
10
  filename?: string;
5
11
  lineno?: number;
6
12
  colno?: number;
7
13
  isNative?: boolean;
14
+ /** Set when the line matched Hermes/minified `func@line:col` (no path). */
15
+ releaseLine?: boolean;
16
+ }
17
+ export interface StackFrameParseOptions {
18
+ /**
19
+ * Hermes / minified RN release lines use `func@line:col` with no file path. Those frames are tagged
20
+ * with `releaseLine` and use this as `filename` (default `bundle.js`). Match the Metro plugin’s
21
+ * source map `file` field (or `sourceMapFile` option there).
22
+ */
23
+ releaseBundleFilename?: string;
8
24
  }
9
25
  /**
10
26
  * Parse a single stack trace line into a structured frame
@@ -17,11 +33,11 @@ export declare function parseStackTrace(stackTrace: string): ParsedStackFrame[];
17
33
  /**
18
34
  * Convert parsed stack frames to Faro's StackFrame format
19
35
  */
20
- export declare function toFaroStackFrames(parsedFrames: ParsedStackFrame[]): StackFrame[];
36
+ export declare function toFaroStackFrames(parsedFrames: ParsedStackFrame[], options?: StackFrameParseOptions): StackFrame[];
21
37
  /**
22
38
  * Extract and parse stack frames from an Error object
23
39
  */
24
- export declare function getStackFramesFromError(error: Error): StackFrame[];
40
+ export declare function getStackFramesFromError(error: Error, options?: StackFrameParseOptions): StackFrame[];
25
41
  /**
26
42
  * Get platform-specific error context
27
43
  */
@@ -29,7 +45,7 @@ export declare function getPlatformErrorContext(): Record<string, string>;
29
45
  /**
30
46
  * Enhance error with React Native specific information
31
47
  */
32
- export declare function enhanceErrorWithContext(error: Error, additionalContext?: Record<string, string>): {
48
+ export declare function enhanceErrorWithContext(error: Error, additionalContext?: Record<string, string>, stackParseOptions?: StackFrameParseOptions): {
33
49
  error: Error;
34
50
  stackFrames: StackFrame[];
35
51
  context: Record<string, string>;
@@ -0,0 +1,87 @@
1
+ #!/bin/sh
2
+ # Upload composed Hermes source map (iOS Release).
3
+ # Invoked from an autolinked Xcode Run Script phase. Expects FARO_BUNDLE_ID,
4
+ # FARO_SOURCEMAP_ENDPOINT, FARO_SOURCEMAP_APP_ID, FARO_SOURCEMAP_STACK_ID,
5
+ # FARO_SOURCEMAP_API_KEY, and optional FARO_SKIP_SOURCEMAP_UPLOAD (same names as `faro-cli metro upload`).
6
+ # Never fails the build — logs warnings and exits 0 when skipping.
7
+
8
+ set -e
9
+
10
+ # Source ios/.xcode.env then ios/.xcode.env.local before any FARO_* decisions so
11
+ # skips and credentials can live there (local overrides base; matches RN with-environment.sh).
12
+ if [ -n "${SRCROOT:-}" ]; then
13
+ if [ -f "$SRCROOT/.xcode.env" ]; then
14
+ set -a
15
+ # shellcheck disable=SC1090
16
+ . "$SRCROOT/.xcode.env"
17
+ set +a
18
+ fi
19
+ if [ -f "$SRCROOT/.xcode.env.local" ]; then
20
+ set -a
21
+ # shellcheck disable=SC1090
22
+ . "$SRCROOT/.xcode.env.local"
23
+ set +a
24
+ fi
25
+ fi
26
+
27
+ # Xcode CONFIGURATION is typically "Debug" or "Release"
28
+ case "$(printf '%s' "${CONFIGURATION:-}" | tr '[:upper:]' '[:lower:]')" in
29
+ release) ;;
30
+ *)
31
+ echo "[Faro] Skipping composed source map upload (configuration: ${CONFIGURATION:-unknown}, not Release)."
32
+ exit 0
33
+ ;;
34
+ esac
35
+
36
+ case "${FARO_SKIP_SOURCEMAP_UPLOAD:-}" in
37
+ 1 | true | TRUE)
38
+ echo "[Faro] Skipping composed source map upload (FARO_SKIP_SOURCEMAP_UPLOAD=${FARO_SKIP_SOURCEMAP_UPLOAD})."
39
+ exit 0
40
+ ;;
41
+ esac
42
+
43
+ # Application JS root (parent of ios/)
44
+ JS_ROOT="${SRCROOT}/.."
45
+ CLI_PATH="$JS_ROOT/node_modules/@grafana/faro-metro-plugin/bin/faro-upload-source-map.js"
46
+
47
+ if [ ! -f "$CLI_PATH" ]; then
48
+ echo "warning: [Faro] Skipping composed source map upload — shim not found at ${CLI_PATH}. Install @grafana/faro-metro-plugin (devDependency) alongside @grafana/faro-react-native."
49
+ exit 0
50
+ fi
51
+
52
+ # React Native Hermes pipeline writes the composed map to SOURCEMAP_FILE when set in
53
+ # Xcode build settings (see RN release / FE O11y docs). Fallback for common layouts.
54
+ MAP="${SOURCEMAP_FILE:-}"
55
+ if [ -z "$MAP" ]; then
56
+ MAP="${CONFIGURATION_BUILD_DIR}/main.jsbundle.map"
57
+ fi
58
+
59
+ if [ ! -f "$MAP" ]; then
60
+ echo "warning: [Faro] Skipping composed source map upload — map not found at ${MAP}."
61
+ echo "warning: [Faro] For iOS Release + Hermes, set SOURCEMAP_FILE in Xcode Release (e.g. \$(DERIVED_FILE_DIR)/main.jsbundle.map) so react-native-xcode.sh composes main.jsbundle.map."
62
+ exit 0
63
+ fi
64
+
65
+ MISSING=""
66
+ [ -z "${FARO_BUNDLE_ID:-}" ] && MISSING="${MISSING}FARO_BUNDLE_ID "
67
+ [ -z "${FARO_SOURCEMAP_ENDPOINT:-}" ] && MISSING="${MISSING}FARO_SOURCEMAP_ENDPOINT "
68
+ [ -z "${FARO_SOURCEMAP_APP_ID:-}" ] && MISSING="${MISSING}FARO_SOURCEMAP_APP_ID "
69
+ [ -z "${FARO_SOURCEMAP_STACK_ID:-}" ] && MISSING="${MISSING}FARO_SOURCEMAP_STACK_ID "
70
+ [ -z "${FARO_SOURCEMAP_API_KEY:-}" ] && MISSING="${MISSING}FARO_SOURCEMAP_API_KEY "
71
+
72
+ if [ -n "$MISSING" ]; then
73
+ echo "warning: [Faro] Skipping composed source map upload — missing env: ${MISSING}"
74
+ echo "warning: [Faro] Export FARO_BUNDLE_ID and FARO_SOURCEMAP_* in the shell running xcodebuild, or add them to ios/.xcode.env.local."
75
+ exit 0
76
+ fi
77
+
78
+ NODE_BIN="${NODE_BINARY:-node}"
79
+
80
+ exec "$NODE_BIN" "$CLI_PATH" \
81
+ --verbose \
82
+ --map "$MAP" \
83
+ --endpoint "$FARO_SOURCEMAP_ENDPOINT" \
84
+ --app-id "$FARO_SOURCEMAP_APP_ID" \
85
+ --stack-id "$FARO_SOURCEMAP_STACK_ID" \
86
+ --api-key "$FARO_SOURCEMAP_API_KEY" \
87
+ --bundle-id "$FARO_BUNDLE_ID"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafana/faro-react-native",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Faro instrumentations, metas, transports for React Native.",
5
5
  "keywords": [
6
6
  "observability",
@@ -61,16 +61,12 @@
61
61
  "react-native-device-info": "^11.1.0"
62
62
  },
63
63
  "peerDependencies": {
64
- "@grafana/faro-react-native-tracing": ">=1.0.0",
65
64
  "@react-navigation/native": ">=6.0.0",
66
65
  "react": ">=18.0.0",
67
66
  "react-native": ">=0.70.0",
68
67
  "react-native-mmkv": ">=2.0.0"
69
68
  },
70
69
  "peerDependenciesMeta": {
71
- "@grafana/faro-react-native-tracing": {
72
- "optional": true
73
- },
74
70
  "@react-navigation/native": {
75
71
  "optional": true
76
72
  },
@@ -79,13 +75,14 @@
79
75
  }
80
76
  },
81
77
  "devDependencies": {
82
- "@types/node": "24.10.1",
83
- "@types/react": "^19.1.1",
84
- "@types/react-native": "^0.73.0",
85
- "react-native-mmkv": "^4.3.1"
78
+ "@types/node": "24.12.3",
79
+ "@types/react": "19.2.14",
80
+ "@types/react-native": "0.73.0",
81
+ "react-native-mmkv": "4.3.1",
82
+ "react-native-nitro-modules": "0.35.6"
86
83
  },
87
84
  "publishConfig": {
88
85
  "access": "public"
89
86
  },
90
- "gitHead": "8ca367978d53ef163886a2e99690c6f8a61416dc"
87
+ "gitHead": "65b49f4c649e3400c74495257d33bd28d9af4b58"
91
88
  }
@@ -1,8 +1,16 @@
1
1
  module.exports = {
2
2
  dependency: {
3
3
  platforms: {
4
- ios: {},
5
4
  android: {},
5
+ ios: {
6
+ scriptPhases: [
7
+ {
8
+ name: '[Faro] Upload composed source map (Release)',
9
+ path: './ios/faro-upload-composed-source-map.sh',
10
+ execution_position: 'any',
11
+ },
12
+ ],
13
+ },
6
14
  },
7
15
  },
8
16
  };
@@ -79,7 +79,11 @@ export function getRNInstrumentations(config: Partial<ReactNativeConfig> = {}):
79
79
 
80
80
  // Error reporting (Flutter: enableFlutterErrorReporting)
81
81
  if (enableErrorReporting) {
82
- instrumentations.push(new ErrorsInstrumentation());
82
+ instrumentations.push(
83
+ new ErrorsInstrumentation({
84
+ releaseBundleFilename: config.releaseBundleFilename,
85
+ })
86
+ );
83
87
  }
84
88
 
85
89
  // Console capture - not in Flutter SDK, RN-specific
@@ -127,4 +127,15 @@ describe('makeRNConfig', () => {
127
127
  (cfg as { preloadedSessionDeviceAttributes?: { device_id: string } }).preloadedSessionDeviceAttributes?.device_id
128
128
  ).toBe('test-preloaded-id');
129
129
  });
130
+
131
+ it('uses releaseBundleFilename in default parseStacktrace when set', () => {
132
+ const cfg = makeRNConfig({
133
+ ...base,
134
+ releaseBundleFilename: 'main.jsbundle',
135
+ });
136
+ const err = new Error('x');
137
+ err.stack = 'g@1:2';
138
+ const parsed = cfg.parseStacktrace?.(err);
139
+ expect(parsed?.frames?.[0]?.filename).toBe('main.jsbundle');
140
+ });
130
141
  });
@@ -65,9 +65,11 @@ function buildInstrumentations(config: ReactNativeConfig): Config['instrumentati
65
65
  /**
66
66
  * React Native stacktrace parser. Uses getStackFramesFromError for RN-specific formats.
67
67
  */
68
- const parseStacktrace: Config['parseStacktrace'] = (err) => ({
69
- frames: getStackFramesFromError(err),
70
- });
68
+ function createParseStacktrace(releaseBundleFilename: string | undefined): Config['parseStacktrace'] {
69
+ return (err) => ({
70
+ frames: getStackFramesFromError(err, { releaseBundleFilename }),
71
+ });
72
+ }
71
73
 
72
74
  /**
73
75
  * Creates a full Faro config from React Native flag-based config.
@@ -86,6 +88,7 @@ export function makeRNConfig(
86
88
  const transports = buildTransports(config);
87
89
  const instrumentations = buildInstrumentations(config);
88
90
 
91
+ const releaseBundleFilename = config.releaseBundleFilename;
89
92
  return {
90
93
  app: config.app,
91
94
  ...(preloadedSessionDeviceAttributes != null && {
@@ -95,7 +98,7 @@ export function makeRNConfig(
95
98
  globalObjectKey: config.globalObjectKey ?? defaultGlobalObjectKey,
96
99
  internalLoggerLevel: config.internalLoggerLevel ?? InternalLoggerLevel.ERROR,
97
100
  isolate: config.isolate ?? false,
98
- parseStacktrace: config.parseStacktrace ?? parseStacktrace,
101
+ parseStacktrace: config.parseStacktrace ?? createParseStacktrace(releaseBundleFilename),
99
102
  paused: config.paused ?? false,
100
103
  preventGlobalExposure: config.preventGlobalExposure ?? false,
101
104
  unpatchedConsole: config.unpatchedConsole ?? defaultUnpatchedConsole,
@@ -70,6 +70,11 @@ type ReactNativeConfigOverrides = {
70
70
  consoleCaptureOptions?: Config['consoleInstrumentation'];
71
71
  enableUserActions?: boolean;
72
72
  userActionsOptions?: Config['userActionsInstrumentation'];
73
+ /**
74
+ * Hermes / minified release stack lines omit a file path. This must match the Metro plugin
75
+ * source map top-level `file` (default `bundle.js`, or your `sourceMapFile` option there).
76
+ */
77
+ releaseBundleFilename?: string;
73
78
  };
74
79
 
75
80
  /**
@@ -2,4 +2,4 @@
2
2
  * NPM package identity for this build. Generated by scripts/sync-faro-rn-package-meta.mjs - do not edit.
3
3
  */
4
4
  export const FARO_REACT_NATIVE_NPM_NAME = '@grafana/faro-react-native' as const;
5
- export const FARO_REACT_NATIVE_NPM_VERSION = '1.1.0' as const;
5
+ export const FARO_REACT_NATIVE_NPM_VERSION = '1.2.0' as const;
@@ -8,10 +8,17 @@ import { SessionInstrumentation } from './instrumentations/session';
8
8
  import * as sessionAttributes from './instrumentations/session/sessionAttributes';
9
9
 
10
10
  describe('initializeFaro', () => {
11
+ const preambleKey = '__faroBundleId_test';
12
+
11
13
  beforeEach(() => {
12
14
  jest.clearAllMocks();
13
15
  jest.clearAllTimers();
14
16
  delete (global as any).faro;
17
+ delete (globalThis as Record<string, unknown>)[preambleKey];
18
+ });
19
+
20
+ afterEach(() => {
21
+ delete (globalThis as Record<string, unknown>)[preambleKey];
15
22
  });
16
23
 
17
24
  it('should initialize Faro', async () => {
@@ -123,4 +130,22 @@ describe('initializeFaro', () => {
123
130
 
124
131
  spy.mockRestore();
125
132
  });
133
+
134
+ it('should set meta.app.bundleId from Faro Metro preamble global (__faroBundleId_<app.name>)', async () => {
135
+ (globalThis as Record<string, unknown>)[preambleKey] = 'release-bundle-from-metro';
136
+
137
+ const faro = await initializeFaro(
138
+ mockConfig({
139
+ url: 'http://localhost:12345/collect',
140
+ transports: [new MockTransport()],
141
+ instrumentations: [new SessionInstrumentation()],
142
+ sessionTracking: {
143
+ enabled: true,
144
+ persistent: false,
145
+ },
146
+ })
147
+ );
148
+
149
+ expect(faro.metas.value.app?.bundleId).toBe('release-bundle-from-metro');
150
+ });
126
151
  });