@luciq/react-native 19.2.1 → 19.3.0-40271-SNAPSHOT

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 (52) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +87 -0
  3. package/RNLuciq.podspec +1 -1
  4. package/android/native.gradle +1 -1
  5. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqAPMModule.java +211 -117
  6. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqNetworkLoggerModule.java +29 -7
  7. package/android/src/main/java/ai/luciq/reactlibrary/RNLuciqReactnativeModule.java +51 -9
  8. package/android/src/main/java/ai/luciq/reactlibrary/utils/EventEmitterModule.java +7 -0
  9. package/dist/constants/Strings.d.ts +9 -0
  10. package/dist/constants/Strings.js +12 -0
  11. package/dist/index.d.ts +2 -1
  12. package/dist/index.js +2 -1
  13. package/dist/models/CustomSpan.d.ts +47 -0
  14. package/dist/models/CustomSpan.js +82 -0
  15. package/dist/modules/APM.d.ts +58 -0
  16. package/dist/modules/APM.js +62 -0
  17. package/dist/modules/Luciq.js +2 -1
  18. package/dist/modules/NetworkLogger.d.ts +0 -5
  19. package/dist/modules/NetworkLogger.js +9 -1
  20. package/dist/native/NativeAPM.d.ts +3 -0
  21. package/dist/native/NativeLuciq.d.ts +1 -0
  22. package/dist/utils/CustomSpansManager.d.ts +38 -0
  23. package/dist/utils/CustomSpansManager.js +173 -0
  24. package/dist/utils/FeatureFlags.d.ts +6 -0
  25. package/dist/utils/FeatureFlags.js +35 -0
  26. package/dist/utils/LuciqUtils.js +6 -0
  27. package/dist/utils/XhrNetworkInterceptor.js +85 -53
  28. package/ios/RNLuciq/LuciqAPMBridge.h +13 -0
  29. package/ios/RNLuciq/LuciqAPMBridge.m +55 -0
  30. package/ios/RNLuciq/LuciqReactBridge.m +12 -0
  31. package/ios/RNLuciq/Util/LCQAPM+PrivateAPIs.h +1 -0
  32. package/ios/native.rb +1 -1
  33. package/package.json +1 -2
  34. package/plugin/build/index.js +9 -2
  35. package/plugin/src/withLuciqIOS.ts +9 -2
  36. package/scripts/releases/changelog_to_slack_formatter.sh +9 -0
  37. package/scripts/releases/get_job_approver.sh +60 -0
  38. package/scripts/releases/get_release_notes.sh +22 -0
  39. package/scripts/releases/get_sdk_version.sh +5 -0
  40. package/scripts/releases/get_slack_id_from_username.sh +24 -0
  41. package/src/constants/Strings.ts +24 -0
  42. package/src/index.ts +2 -0
  43. package/src/models/CustomSpan.ts +102 -0
  44. package/src/modules/APM.ts +72 -0
  45. package/src/modules/Luciq.ts +3 -1
  46. package/src/modules/NetworkLogger.ts +26 -1
  47. package/src/native/NativeAPM.ts +7 -0
  48. package/src/native/NativeLuciq.ts +1 -0
  49. package/src/utils/CustomSpansManager.ts +202 -0
  50. package/src/utils/FeatureFlags.ts +44 -0
  51. package/src/utils/LuciqUtils.ts +15 -0
  52. package/src/utils/XhrNetworkInterceptor.ts +128 -55
@@ -90,6 +90,61 @@ RCT_EXPORT_METHOD(setScreenRenderingEnabled:(BOOL)isEnabled) {
90
90
  LCQAPM.screenRenderingEnabled = isEnabled;
91
91
  }
92
92
 
93
+ // Syncs a custom span to the native SDK (currently logs only)
94
+ RCT_EXPORT_METHOD(syncCustomSpan:(NSString *)name
95
+ startTimestamp:(double)startTimestamp
96
+ endTimestamp:(double)endTimestamp
97
+ resolver:(RCTPromiseResolveBlock)resolve
98
+ rejecter:(RCTPromiseRejectBlock)reject)
99
+ {
100
+ @try {
101
+ // Convert microseconds → seconds (NSDate uses seconds)
102
+ NSTimeInterval startSeconds = startTimestamp / 1e6;
103
+ NSTimeInterval endSeconds = endTimestamp / 1e6;
104
+
105
+ NSDate *startDate = [NSDate dateWithTimeIntervalSince1970:startSeconds];
106
+ NSDate *endDate = [NSDate dateWithTimeIntervalSince1970:endSeconds];
107
+
108
+ // Add completed span to APM
109
+ [LCQAPM addCompletedCustomSpanWithName:name
110
+ startDate:startDate
111
+ endDate:endDate];
112
+
113
+ resolve(@YES);
114
+ }
115
+ @catch (NSException *exception) {
116
+ reject(
117
+ @"SYNC_CUSTOM_SPAN_ERROR",
118
+ exception.reason ?: @"Failed to sync custom span",
119
+ nil
120
+ );
121
+ }
122
+ }
123
+
124
+ // Checks if custom spans feature is enabled
125
+ RCT_EXPORT_METHOD(isCustomSpanEnabled:(RCTPromiseResolveBlock)resolve
126
+ rejecter:(RCTPromiseRejectBlock)reject) {
127
+ @try {
128
+ BOOL enabled = LCQAPM.customSpansEnabled;
129
+ resolve(@(enabled));
130
+ } @catch (NSException *exception) {
131
+ NSLog(@"[CustomSpan] Error checking feature flag: %@", exception);
132
+ resolve(@NO);
133
+ }
134
+ }
135
+
136
+ // Checks if APM is enabled
137
+ RCT_EXPORT_METHOD(isAPMEnabled:(RCTPromiseResolveBlock)resolve
138
+ rejecter:(RCTPromiseRejectBlock)reject) {
139
+ @try {
140
+ BOOL enabled = LCQAPM.enabled;
141
+ resolve(@(enabled));
142
+ } @catch (NSException *exception) {
143
+ NSLog(@"[CustomSpan] Error checking APM enabled: %@", exception);
144
+ resolve(@NO);
145
+ }
146
+ }
147
+
93
148
 
94
149
 
95
150
  @synthesize description;
@@ -545,4 +545,16 @@ RCT_EXPORT_METHOD(setNetworkLogBodyEnabled:(BOOL)isEnabled) {
545
545
  LCQNetworkLogger.logBodyEnabled = isEnabled;
546
546
  }
547
547
 
548
+ // Checks if Luciq SDK is initialized
549
+ RCT_EXPORT_METHOD(isBuilt:(RCTPromiseResolveBlock)resolve
550
+ rejecter:(RCTPromiseRejectBlock)reject) {
551
+ @try {
552
+ BOOL isBuilt = YES;
553
+ resolve(@(isBuilt));
554
+ } @catch (NSException *exception) {
555
+ NSLog(@"[Luciq] Error checking if SDK is built: %@", exception);
556
+ resolve(@NO);
557
+ }
558
+ }
559
+
548
560
  @end
@@ -11,5 +11,6 @@
11
11
 
12
12
  @property (class, atomic, assign) BOOL networkEnabled;
13
13
 
14
+ + (BOOL)customSpansEnabled;
14
15
 
15
16
  @end
package/ios/native.rb CHANGED
@@ -1,4 +1,4 @@
1
- $luciq= { :version => '19.4.0' }
1
+ $luciq= { :version => '19.5.0' }
2
2
 
3
3
  def use_luciq! (spec = nil)
4
4
  version = $luciq[:version]
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@luciq/react-native",
3
3
  "description": "Luciq is the Agentic Observability Platform built for Mobile.",
4
- "version": "19.2.1",
4
+ "version": "19.3.0-40271-SNAPSHOT",
5
5
  "author": "Luciq (https://luciq.ai)",
6
6
  "repository": "github:luciqai/luciq-reactnative-sdk",
7
7
  "homepage": "https://www.luciq.ai/platforms/react-native",
@@ -43,7 +43,6 @@
43
43
  },
44
44
  "devDependencies": {
45
45
  "@apollo/client": "^3.7.0",
46
- "@instabug/danger-plugin-coverage": "Instabug/danger-plugin-coverage",
47
46
  "@react-native-community/eslint-config": "^3.1.0",
48
47
  "@react-navigation/native": "^6.1.7",
49
48
  "@rollup/plugin-commonjs": "^25.0.3",
@@ -42044,9 +42044,16 @@ function addLuciqBuildPhase(xcodeProject, packageName) {
42044
42044
  // Inject source map export line into the shell script
42045
42045
  function injectSourceMapExport(script) {
42046
42046
  var exportLine = 'export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"';
42047
- var escapedLine = exportLine.replace(/\$/g, '\\$').replace(/"/g, '\\"');
42047
+ var escapedLine = exportLine.replace(/"/g, '\\"');
42048
42048
  var injectedLine = "".concat(escapedLine, "\\n");
42049
- return script.includes(escapedLine) ? script : script.replace(/^"/, "\"".concat(injectedLine));
42049
+ if (script.includes(escapedLine)) {
42050
+ return script;
42051
+ }
42052
+ var buggyLine = exportLine.replace(/\$/g, '\\$').replace(/"/g, '\\"');
42053
+ if (script.includes(buggyLine)) {
42054
+ return script.split(buggyLine).join(escapedLine);
42055
+ }
42056
+ return script.replace(/^"/, "\"".concat(injectedLine));
42050
42057
  }
42051
42058
 
42052
42059
  var luciqPackage = require('../../package.json');
@@ -102,8 +102,15 @@ function addLuciqBuildPhase(xcodeProject: XcodeProject, packageName: string): vo
102
102
  // Inject source map export line into the shell script
103
103
  function injectSourceMapExport(script: string): string {
104
104
  const exportLine = 'export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"';
105
- const escapedLine = exportLine.replace(/\$/g, '\\$').replace(/"/g, '\\"');
105
+ const escapedLine = exportLine.replace(/"/g, '\\"');
106
106
  const injectedLine = `${escapedLine}\\n`;
107
107
 
108
- return script.includes(escapedLine) ? script : script.replace(/^"/, `"${injectedLine}`);
108
+ if (script.includes(escapedLine)) {
109
+ return script;
110
+ }
111
+ const buggyLine = exportLine.replace(/\$/g, '\\$').replace(/"/g, '\\"');
112
+ if (script.includes(buggyLine)) {
113
+ return script.split(buggyLine).join(escapedLine);
114
+ }
115
+ return script.replace(/^"/, `"${injectedLine}`);
109
116
  }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ input=$(cat)
3
+
4
+ input=$(sed -E \
5
+ -e 's/\[([^]]+)\]\(([^)]+)\)/<\2|\1>/g' \
6
+ -e 's/^#{1,6}[[:space:]]*([^[:space:]].*)$/\*\1\*/' \
7
+ -e 's/^- /• /' <<< "$input")
8
+
9
+ echo "$input"
@@ -0,0 +1,60 @@
1
+ if [ -z "$CIRCLE_TOKEN" ]; then
2
+ echo "Error: CIRCLE_TOKEN is not set" >&2
3
+ exit 1
4
+ fi
5
+
6
+ if [ -z "$CIRCLE_WORKFLOW_ID" ]; then
7
+ if [ -z "$CIRCLE_PIPELINE_ID" ]; then
8
+ echo "Error: Neither CIRCLE_WORKFLOW_ID nor CIRCLE_PIPELINE_ID is set" >&2
9
+ exit 1
10
+ fi
11
+ pipelineJson=$(curl -s -X GET "https://circleci.com/api/v2/pipeline/$CIRCLE_PIPELINE_ID/workflow" --header "Circle-Token: $CIRCLE_TOKEN")
12
+ CIRCLE_WORKFLOW_ID=$(echo "$pipelineJson" | jq -r '.items[0].id // empty')
13
+ if [ -z "$CIRCLE_WORKFLOW_ID" ]; then
14
+ echo "Error: Failed to get workflow ID from pipeline" >&2
15
+ exit 1
16
+ fi
17
+ fi
18
+
19
+ response=$(curl -s -w "\n%{http_code}" -X GET "https://circleci.com/api/v2/workflow/$CIRCLE_WORKFLOW_ID/job" --header "Circle-Token: $CIRCLE_TOKEN")
20
+ http_code=$(echo "$response" | tail -n1)
21
+ jobsJson=$(echo "$response" | sed '$d')
22
+
23
+ if [ "$http_code" != "200" ]; then
24
+ echo "Error: CircleCI API returned HTTP $http_code" >&2
25
+ echo "Workflow ID: $CIRCLE_WORKFLOW_ID" >&2
26
+ echo "Response: $jobsJson" >&2
27
+ exit 1
28
+ fi
29
+
30
+ if [ -z "$jobsJson" ]; then
31
+ echo "Error: Failed to fetch jobs from CircleCI API (empty response)" >&2
32
+ exit 1
33
+ fi
34
+
35
+ if ! echo "$jobsJson" | jq -e '.items' > /dev/null 2>&1; then
36
+ echo "Error: Invalid JSON response from CircleCI API" >&2
37
+ echo "Response: $jobsJson" >&2
38
+ exit 1
39
+ fi
40
+
41
+ job=$(jq '.items[] | select(.name == "hold_publish" or .name == "hold_slack_notification") | select(.approved_by != null)' <<< "$jobsJson")
42
+
43
+ if [ -z "$job" ] || [ "$job" == "null" ]; then
44
+ echo "Error: Could not find approved job in workflow" >&2
45
+ exit 1
46
+ fi
47
+
48
+ approver_id=$(jq '.approved_by' <<< "$job")
49
+
50
+ approver_id=$(tr -d '"' <<< "$approver_id")
51
+
52
+ user=$(curl -s -X GET "https://circleci.com/api/v2/user/$approver_id" --header "Circle-Token: $CIRCLE_TOKEN")
53
+
54
+ username=$(jq '.login' <<< "$user")
55
+
56
+ username=$(tr -d '"' <<< "$username")
57
+
58
+ slack_id=$(./scripts/releases/get_slack_id_from_username.sh "$username")
59
+
60
+ echo "$slack_id"
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ latest_release=""
3
+ capturing=false
4
+
5
+ while IFS= read -r line; do
6
+ if [[ "$line" == "## ["* ]]; then
7
+ if $capturing; then
8
+ break
9
+ fi
10
+ fi
11
+
12
+ if [[ "$line" == "### "* ]]; then
13
+ capturing=true
14
+ fi
15
+
16
+ if $capturing; then
17
+ line=$(./scripts/releases/changelog_to_slack_formatter.sh <<< "$line")
18
+ latest_release+="$line\n"
19
+ fi
20
+ done < CHANGELOG.md
21
+
22
+ echo "$latest_release"
@@ -0,0 +1,5 @@
1
+ sdk_version=$(grep -i 'version' package.json) #"version": "xx.x.x+x",
2
+ sdk_version=$(cut -f2 -d' ' <<< $sdk_version | tr -d '" ,') #xx.x.x+x,
3
+ sdk_version=$(cut -f1 -d'+' <<< $sdk_version) #xx.x.x
4
+
5
+ echo "$sdk_version"
@@ -0,0 +1,24 @@
1
+ github_username=$1
2
+
3
+ case $github_username in
4
+ 'mzelzoghbi')
5
+ sid='U5697F4EL'
6
+ ;;
7
+ 'AndrewAminInstabug')
8
+ sid='U06JVRNMKE1'
9
+ ;;
10
+ 'ahmedAlaaInstabug')
11
+ sid='U06AE2G1161'
12
+ ;;
13
+ 'kholood-ea')
14
+ sid='U06SU2QR280'
15
+ ;;
16
+ 'AyaMahmoud148')
17
+ sid='U07GZSURC8K'
18
+ ;;
19
+ 'MoKamall')
20
+ sid='U06JHDS3JJK'
21
+ ;;
22
+ *)
23
+ esac
24
+ echo "$sid"
@@ -0,0 +1,24 @@
1
+ export class LuciqStrings {
2
+ static readonly customSpanAPMDisabledMessage: string =
3
+ 'APM is disabled, custom span not created. Please enable APM by following the instructions at this link:\n' +
4
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
5
+
6
+ static readonly customSpanDisabled: string =
7
+ 'Custom span is disabled, custom span not created. Please enable Custom Span by following the instructions at this link:\n' +
8
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
9
+
10
+ static readonly customSpanSDKNotInitializedMessage: string =
11
+ 'Luciq API was called before the SDK is built. To build it, first by following the instructions at this link:\n' +
12
+ 'https://docs.luciq.ai/product-guides-and-integrations/product-guides/application-performance-monitoring';
13
+
14
+ static readonly customSpanNameEmpty: string =
15
+ 'Custom span name cannot be empty. Please provide a valid name for the custom span.';
16
+
17
+ static readonly customSpanEndTimeBeforeStartTime: string =
18
+ 'Custom span end time must be after start time. Please provide a valid start and end time for the custom span.';
19
+
20
+ static readonly customSpanNameTruncated: string = 'Custom span name truncated to 150 characters';
21
+
22
+ static readonly customSpanLimitReached: string =
23
+ 'Maximum number of concurrent custom spans (100) reached. Please end some spans before starting new ones.';
24
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import type { LuciqConfig } from './models/LuciqConfig';
3
3
  import Report from './models/Report';
4
4
  import type { ThemeConfig } from './models/ThemeConfig';
5
+ import { CustomSpan } from './models/CustomSpan';
5
6
  // Modules
6
7
  import * as APM from './modules/APM';
7
8
  import * as BugReporting from './modules/BugReporting';
@@ -23,6 +24,7 @@ import type { SessionMetadata } from './models/SessionMetadata';
23
24
  export * from './utils/Enums';
24
25
  export {
25
26
  Report,
27
+ CustomSpan,
26
28
  APM,
27
29
  BugReporting,
28
30
  CrashReporting,
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Callback to unregister a span from tracking
3
+ */
4
+ type UnregisterCallback = (span: CustomSpan) => void;
5
+
6
+ /**
7
+ * Callback to sync span data to native SDK
8
+ */
9
+ type SyncCallback = (name: string, startTimestamp: number, endTimestamp: number) => Promise<void>;
10
+
11
+ /**
12
+ * Represents a custom span for performance tracking.
13
+ * A span measures the duration of an operation and reports it to the native SDK.
14
+ */
15
+ export class CustomSpan {
16
+ private name: string;
17
+ private startTime: number; // Date.now() in milliseconds
18
+ private startMonotonic: number; // performance.now() in milliseconds
19
+ private endTime?: number;
20
+ private duration?: number;
21
+ private hasEnded: boolean = false;
22
+ private endPromise?: Promise<void>;
23
+ private unregisterCallback: UnregisterCallback;
24
+ private syncCallback: SyncCallback;
25
+
26
+ /**
27
+ * Creates a new custom span. The span starts immediately upon creation.
28
+ * @internal - Use APM.startCustomSpan() instead
29
+ */
30
+ constructor(name: string, unregisterCallback: UnregisterCallback, syncCallback: SyncCallback) {
31
+ this.name = name;
32
+ this.startTime = Date.now();
33
+ this.startMonotonic = performance.now();
34
+ this.unregisterCallback = unregisterCallback;
35
+ this.syncCallback = syncCallback;
36
+ }
37
+
38
+ /**
39
+ * Ends this custom span and reports it to the native SDK.
40
+ * This method is idempotent - calling it multiple times is safe.
41
+ * Subsequent calls will wait for the first call to complete.
42
+ */
43
+ async end(): Promise<void> {
44
+ // Thread-safe check using Promise-based locking
45
+ if (this.hasEnded) {
46
+ if (this.endPromise) {
47
+ await this.endPromise;
48
+ }
49
+ return;
50
+ }
51
+
52
+ // Create lock and mark as ended
53
+ let resolveEnd: () => void;
54
+ this.endPromise = new Promise((resolve) => {
55
+ resolveEnd = resolve;
56
+ });
57
+ this.hasEnded = true;
58
+
59
+ try {
60
+ // Unregister from active spans
61
+ this.unregisterCallback(this);
62
+
63
+ // Calculate duration using monotonic clock
64
+ const endMonotonic = performance.now();
65
+ this.duration = endMonotonic - this.startMonotonic;
66
+
67
+ // Calculate end time using wall clock
68
+ this.endTime = this.startTime + this.duration;
69
+
70
+ // Convert to microseconds for native SDK
71
+ const startMicros = this.startTime * 1000;
72
+ const endMicros = this.endTime * 1000;
73
+
74
+ // Send to native SDK
75
+ await this.syncCallback(this.name, startMicros, endMicros);
76
+ } finally {
77
+ // Release lock
78
+ resolveEnd!();
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get the span name
84
+ */
85
+ getName(): string {
86
+ return this.name;
87
+ }
88
+
89
+ /**
90
+ * Check if the span has ended
91
+ */
92
+ isEnded(): boolean {
93
+ return this.hasEnded;
94
+ }
95
+
96
+ /**
97
+ * Get the span duration in milliseconds (only available after end())
98
+ */
99
+ getDuration(): number | undefined {
100
+ return this.duration;
101
+ }
102
+ }
@@ -2,6 +2,11 @@ import { Platform } from 'react-native';
2
2
 
3
3
  import { NativeAPM } from '../native/NativeAPM';
4
4
  import { NativeLuciq } from '../native/NativeLuciq';
5
+ import {
6
+ startCustomSpan as startCustomSpanInternal,
7
+ addCompletedCustomSpan as addCompletedCustomSpanInternal,
8
+ } from '../utils/CustomSpansManager';
9
+ import type { CustomSpan } from '../models/CustomSpan';
5
10
 
6
11
  /**
7
12
  * Enables or disables APM
@@ -123,3 +128,70 @@ export const _lcqSleep = () => {
123
128
  export const setScreenRenderingEnabled = (isEnabled: boolean) => {
124
129
  NativeAPM.setScreenRenderingEnabled(isEnabled);
125
130
  };
131
+
132
+ /**
133
+ * Starts a custom span for performance tracking.
134
+ *
135
+ * A custom span measures the duration of an arbitrary operation that is not
136
+ * automatically tracked by the SDK. The span must be manually ended by calling
137
+ * the `end()` method on the returned span object.
138
+ *
139
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
140
+ * Leading and trailing whitespace will be trimmed.
141
+ *
142
+ * @returns Promise<CustomSpan | null> - The span object to end later, or null if:
143
+ * - Name is empty after trimming
144
+ * - SDK is not initialized
145
+ * - APM is disabled
146
+ * - Custom spans feature is disabled
147
+ * - Maximum concurrent spans limit (100) reached
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const span = await APM.startCustomSpan('Load User Profile');
152
+ * if (span) {
153
+ * try {
154
+ * // ... perform operation ...
155
+ * } finally {
156
+ * await span.end();
157
+ * }
158
+ * }
159
+ * ```
160
+ */
161
+ export const startCustomSpan = async (name: string): Promise<CustomSpan | null> => {
162
+ return startCustomSpanInternal(name);
163
+ };
164
+
165
+ /**
166
+ * Records a completed custom span with pre-recorded timestamps.
167
+ *
168
+ * Use this method when you have already recorded the start and end times
169
+ * of an operation and want to report it retroactively.
170
+ *
171
+ * @param name - The name of the span. Cannot be empty. Max 150 characters.
172
+ * Leading and trailing whitespace will be trimmed.
173
+ * @param startDate - The start time of the operation
174
+ * @param endDate - The end time of the operation (must be after startDate)
175
+ *
176
+ * @returns Promise<void> - Resolves when the span has been recorded, or logs error if:
177
+ * - Name is empty after trimming
178
+ * - End date is not after start date
179
+ * - SDK is not initialized
180
+ * - APM is disabled
181
+ * - Custom spans feature is disabled
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const start = new Date();
186
+ * // ... operation already completed ...
187
+ * const end = new Date();
188
+ * await APM.addCompletedCustomSpan('Cache Lookup', start, end);
189
+ * ```
190
+ */
191
+ export const addCompletedCustomSpan = async (
192
+ name: string,
193
+ startDate: Date,
194
+ endDate: Date,
195
+ ): Promise<void> => {
196
+ return addCompletedCustomSpanInternal(name, startDate, endDate);
197
+ };
@@ -10,7 +10,7 @@ import type { NavigationAction, NavigationState as NavigationStateV4 } from 'rea
10
10
  import type { LuciqConfig } from '../models/LuciqConfig';
11
11
  import Report from '../models/Report';
12
12
  import { emitter, NativeEvents, NativeLuciq } from '../native/NativeLuciq';
13
- import { registerFeatureFlagsListener } from '../utils/FeatureFlags';
13
+ import { registerFeatureFlagsListener, initFeatureFlagsCache } from '../utils/FeatureFlags';
14
14
  import {
15
15
  AutoMaskingType,
16
16
  ColorTheme,
@@ -81,6 +81,8 @@ function reportCurrentViewForAndroid(screenName: string | null) {
81
81
  * @param config SDK configurations. See {@link LuciqConfig} for more info.
82
82
  */
83
83
  export const init = (config: LuciqConfig) => {
84
+ initFeatureFlagsCache();
85
+
84
86
  if (Platform.OS === 'android') {
85
87
  // Add android feature flags listener for android
86
88
  registerFeatureFlagsListener();
@@ -39,10 +39,17 @@ function getPortFromUrl(url: string) {
39
39
  * It is enabled by default.
40
40
  * @param isEnabled
41
41
  */
42
+ const NET_TAG = 'LCQ-RN-NET:';
43
+
42
44
  export const setEnabled = (isEnabled: boolean) => {
43
45
  if (isEnabled) {
44
46
  xhr.enableInterception();
45
47
  xhr.setOnDoneCallback(async (network) => {
48
+ Logger.debug(
49
+ NET_TAG,
50
+ `[NetworkLogger] onDoneCallback received: ${network.method} ${network.url}, status=${network.responseCode}`,
51
+ );
52
+
46
53
  // eslint-disable-next-line no-new-func
47
54
  const predicate = Function('network', 'return ' + _requestFilterExpression);
48
55
 
@@ -50,12 +57,17 @@ export const setEnabled = (isEnabled: boolean) => {
50
57
  const MAX_NETWORK_BODY_SIZE_IN_BYTES = await NativeLuciq.getNetworkBodyMaxSize();
51
58
  try {
52
59
  if (_networkDataObfuscationHandler) {
60
+ Logger.debug(NET_TAG, `[NetworkLogger] Running obfuscation handler for ${network.url}`);
53
61
  network = await _networkDataObfuscationHandler(network);
54
62
  }
55
63
 
56
64
  if (__DEV__) {
57
65
  const urlPort = getPortFromUrl(network.url);
58
66
  if (urlPort === LuciqRNConfig.metroDevServerPort) {
67
+ Logger.debug(
68
+ NET_TAG,
69
+ `[NetworkLogger] Skipping Metro dev server request: ${network.url}`,
70
+ );
59
71
  return;
60
72
  }
61
73
  }
@@ -97,10 +109,23 @@ export const setEnabled = (isEnabled: boolean) => {
97
109
  );
98
110
  }
99
111
 
112
+ Logger.debug(
113
+ NET_TAG,
114
+ `[NetworkLogger] Reporting network log to native: ${network.method} ${network.url}`,
115
+ );
100
116
  reportNetworkLog(network);
101
117
  } catch (e) {
102
- Logger.error(e);
118
+ Logger.error(
119
+ NET_TAG,
120
+ `[NetworkLogger] Error processing network log for ${network.url}:`,
121
+ e,
122
+ );
103
123
  }
124
+ } else {
125
+ Logger.debug(
126
+ NET_TAG,
127
+ `[NetworkLogger] Request filtered out by predicate: ${network.method} ${network.url}, expression="${_requestFilterExpression}"`,
128
+ );
104
129
  }
105
130
  });
106
131
  } else {
@@ -47,6 +47,13 @@ export interface ApmNativeModule extends NativeModule {
47
47
 
48
48
  // Screen Rendering //
49
49
  setScreenRenderingEnabled(isEnabled: boolean): void;
50
+
51
+ // Custom Spans APIs //
52
+ syncCustomSpan(name: string, startTimestamp: number, endTimestamp: number): Promise<void>;
53
+
54
+ isCustomSpanEnabled(): Promise<boolean>;
55
+
56
+ isAPMEnabled(): Promise<boolean>;
50
57
  }
51
58
 
52
59
  export const NativeAPM = NativeModules.LCQAPM;
@@ -22,6 +22,7 @@ export interface LuciqNativeModule extends NativeModule {
22
22
 
23
23
  // Essential APIs //
24
24
  setEnabled(isEnabled: boolean): void;
25
+ isBuilt(): Promise<boolean>;
25
26
  init(
26
27
  token: string,
27
28
  invocationEvents: InvocationEvent[],