|
1 | 1 | import type { Client, Span } from '@sentry/core'; |
2 | 2 | import { debug, getSpanDescendants, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; |
3 | 3 | import type { AppStateStatus } from 'react-native'; |
4 | | -import { AppState } from 'react-native'; |
| 4 | +import { AppState, Platform } from 'react-native'; |
5 | 5 | import { isRootSpan, isSentrySpan } from '../utils/span'; |
6 | 6 |
|
| 7 | +/** |
| 8 | + * The time to wait after the app enters the `inactive` state on iOS before |
| 9 | + * cancelling the span. |
| 10 | + */ |
| 11 | +const IOS_INACTIVE_CANCEL_DELAY_MS = 5_000; |
| 12 | + |
7 | 13 | /** |
8 | 14 | * Hooks on span end event to execute a callback when the span ends. |
9 | 15 | */ |
@@ -33,8 +39,9 @@ export const adjustTransactionDuration = (client: Client, span: Span, maxDuratio |
33 | 39 | return; |
34 | 40 | } |
35 | 41 |
|
36 | | - const diff = endTimestamp - startTimestamp; |
37 | | - const isOutdatedTransaction = endTimestamp && (diff > maxDurationMs || diff < 0); |
| 42 | + const diff = endTimestamp - startTimestamp; // a diff in *seconds* |
| 43 | + const isOutdatedTransaction = diff > maxDurationMs / 1000 || diff < 0; |
| 44 | + |
38 | 45 | if (isOutdatedTransaction) { |
39 | 46 | span.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); |
40 | 47 | // TODO: check where was used, might be possible to delete |
@@ -174,20 +181,54 @@ export const onlySampleIfChildSpans = (client: Client, span: Span): void => { |
174 | 181 |
|
175 | 182 | /** |
176 | 183 | * Hooks on AppState change to cancel the span if the app goes background. |
| 184 | + * |
| 185 | + * On iOS the JS thread can be suspended between the `inactive` and |
| 186 | + * `background` transitions, which means the `background` event may never |
| 187 | + * reach JS in time. To handle this we schedule a deferred cancellation when |
| 188 | + * the app becomes `inactive`. If the app returns to `active` before the |
| 189 | + * timeout fires, the cancellation is cleared. If it transitions to |
| 190 | + * `background` first, we cancel immediately and clear the timeout. |
177 | 191 | */ |
178 | 192 | export const cancelInBackground = (client: Client, span: Span): void => { |
| 193 | + let inactiveTimeout: ReturnType<typeof setTimeout> | undefined; |
| 194 | + |
| 195 | + const cancelSpan = (): void => { |
| 196 | + if (inactiveTimeout !== undefined) { |
| 197 | + clearTimeout(inactiveTimeout); |
| 198 | + inactiveTimeout = undefined; |
| 199 | + } |
| 200 | + debug.log(`Setting ${spanToJSON(span).op} transaction to cancelled because the app is in the background.`); |
| 201 | + span.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); |
| 202 | + span.end(); |
| 203 | + }; |
| 204 | + |
179 | 205 | const subscription = AppState.addEventListener('change', (newState: AppStateStatus) => { |
180 | 206 | if (newState === 'background') { |
181 | | - debug.log(`Setting ${spanToJSON(span).op} transaction to cancelled because the app is in the background.`); |
182 | | - span.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); |
183 | | - span.end(); |
| 207 | + cancelSpan(); |
| 208 | + } else if (Platform.OS === 'ios' && newState === 'inactive') { |
| 209 | + // Schedule a deferred cancellation — if the JS thread is suspended |
| 210 | + // before the 'background' event fires, this timer will execute when |
| 211 | + // the app is eventually resumed and end the span. |
| 212 | + if (inactiveTimeout === undefined) { |
| 213 | + inactiveTimeout = setTimeout(cancelSpan, IOS_INACTIVE_CANCEL_DELAY_MS); |
| 214 | + } |
| 215 | + } else if (newState === 'active') { |
| 216 | + // App returned to foreground — clear any pending inactive cancellation. |
| 217 | + if (inactiveTimeout !== undefined) { |
| 218 | + clearTimeout(inactiveTimeout); |
| 219 | + inactiveTimeout = undefined; |
| 220 | + } |
184 | 221 | } |
185 | 222 | }); |
186 | 223 |
|
187 | 224 | subscription && |
188 | 225 | client.on('spanEnd', (endedSpan: Span) => { |
189 | 226 | if (endedSpan === span) { |
190 | 227 | debug.log(`Removing AppState listener for ${spanToJSON(span).op} transaction.`); |
| 228 | + if (inactiveTimeout !== undefined) { |
| 229 | + clearTimeout(inactiveTimeout); |
| 230 | + inactiveTimeout = undefined; |
| 231 | + } |
191 | 232 | subscription?.remove?.(); |
192 | 233 | } |
193 | 234 | }); |
|
0 commit comments