@grafana/faro-react-native 1.1.0 → 1.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 +72 -0
- package/android/build.gradle +145 -0
- package/dist/cjs/config/getRNInstrumentations.js +3 -1
- package/dist/cjs/config/getRNInstrumentations.js.map +1 -1
- package/dist/cjs/config/makeRNConfig.js +7 -4
- package/dist/cjs/config/makeRNConfig.js.map +1 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js +1 -1
- package/dist/cjs/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/cjs/instrumentations/errors/index.js +3 -2
- package/dist/cjs/instrumentations/errors/index.js.map +1 -1
- package/dist/cjs/instrumentations/errors/stackTraceParser.js +98 -37
- package/dist/cjs/instrumentations/errors/stackTraceParser.js.map +1 -1
- package/dist/esm/config/getRNInstrumentations.js +3 -1
- package/dist/esm/config/getRNInstrumentations.js.map +1 -1
- package/dist/esm/config/makeRNConfig.js +7 -4
- package/dist/esm/config/makeRNConfig.js.map +1 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js +1 -1
- package/dist/esm/generated/faroRNPackageMeta.js.map +1 -1
- package/dist/esm/instrumentations/errors/index.js +3 -2
- package/dist/esm/instrumentations/errors/index.js.map +1 -1
- package/dist/esm/instrumentations/errors/stackTraceParser.js +97 -37
- package/dist/esm/instrumentations/errors/stackTraceParser.js.map +1 -1
- package/dist/types/config/types.d.ts +5 -0
- package/dist/types/generated/faroRNPackageMeta.d.ts +1 -1
- package/dist/types/instrumentations/errors/index.d.ts +5 -0
- package/dist/types/instrumentations/errors/stackTraceParser.d.ts +19 -3
- package/ios/faro-upload-composed-source-map.sh +93 -0
- package/package.json +7 -10
- package/react-native.config.js +9 -1
- package/src/config/getRNInstrumentations.ts +5 -1
- package/src/config/makeRNConfig.test.ts +11 -0
- package/src/config/makeRNConfig.ts +7 -4
- package/src/config/types.ts +5 -0
- package/src/generated/faroRNPackageMeta.ts +1 -1
- package/src/initialize.test.ts +25 -0
- package/src/instrumentations/errors/index.ts +25 -8
- package/src/instrumentations/errors/stackTraceParser.test.ts +65 -0
- 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
|
-
*
|
|
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
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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 =
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
5
|
+
export declare const FARO_REACT_NATIVE_NPM_VERSION: "1.2.1";
|
|
@@ -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,93 @@
|
|
|
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
|
+
upload_exit=0
|
|
81
|
+
"$NODE_BIN" "$CLI_PATH" \
|
|
82
|
+
--verbose \
|
|
83
|
+
--map "$MAP" \
|
|
84
|
+
--endpoint "$FARO_SOURCEMAP_ENDPOINT" \
|
|
85
|
+
--app-id "$FARO_SOURCEMAP_APP_ID" \
|
|
86
|
+
--stack-id "$FARO_SOURCEMAP_STACK_ID" \
|
|
87
|
+
--api-key "$FARO_SOURCEMAP_API_KEY" \
|
|
88
|
+
--bundle-id "$FARO_BUNDLE_ID" || upload_exit=$?
|
|
89
|
+
|
|
90
|
+
if [ "$upload_exit" -ne 0 ]; then
|
|
91
|
+
echo "warning: [Faro] Composed source map upload failed (exit $upload_exit)"
|
|
92
|
+
fi
|
|
93
|
+
exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafana/faro-react-native",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
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.
|
|
83
|
-
"@types/react": "
|
|
84
|
-
"@types/react-native": "
|
|
85
|
-
"react-native-mmkv": "
|
|
78
|
+
"@types/node": "24.12.4",
|
|
79
|
+
"@types/react": "19.2.15",
|
|
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": "
|
|
87
|
+
"gitHead": "ae48b3fc6f2fc4201d634edd38e2713f459dd008"
|
|
91
88
|
}
|
package/react-native.config.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
69
|
-
|
|
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 ??
|
|
101
|
+
parseStacktrace: config.parseStacktrace ?? createParseStacktrace(releaseBundleFilename),
|
|
99
102
|
paused: config.paused ?? false,
|
|
100
103
|
preventGlobalExposure: config.preventGlobalExposure ?? false,
|
|
101
104
|
unpatchedConsole: config.unpatchedConsole ?? defaultUnpatchedConsole,
|
package/src/config/types.ts
CHANGED
|
@@ -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
|
|
5
|
+
export const FARO_REACT_NATIVE_NPM_VERSION = '1.2.1' as const;
|
package/src/initialize.test.ts
CHANGED
|
@@ -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
|
});
|