Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
6 changes: 1 addition & 5 deletions contributingGuides/LOADING_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ const shouldShowLoadingIndicator = isAppLoadPending && !isOffline;

// ...
{shouldShowLoadingIndicator ? (
<ActivityIndicator
reasonAttributes={{context: 'WorkspacesListPage', isOffline} satisfies SkeletonSpanReasonAttributes}
/>
<ActivityIndicator />
) : (
<WorkspaceListTable workspaces={workspaceRows} />
)}
Expand Down Expand Up @@ -172,5 +170,3 @@ Keep `HAS_LOADED_APP` and the cold-restart fallback. The queue hook is the prima
Report skeleton consumers use `useIsReportLoadPending(reportID)` wherever pending `OpenReport` work is part of the loading decision. They keep existing readiness checks, including `hasOnceLoadedReportActions`, report data completeness, and offline behavior. A stranded `isLoadingInitialReportActions` value without a matching queue request or in-memory latch must not show a skeleton.

Do not remove the legacy fields as part of this migration. `IS_LOADING_APP` and report loading state still support recovery, report positioning, navigation guards, and the deferred-flush bridge. Skeleton consumers should use the public hooks. Full flag deletion is outside this plan.

**Keep telemetry for a terminal request that still shows a skeleton.** `useSkeletonSpan` in `src/libs/telemetry/useSkeletonSpan.ts` and the `reasonAttributes` prop on `ActivityIndicator` record a span while a skeleton is mounted. They flag skeletons that remain past `CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION`. Pass `SkeletonSpanReasonAttributes` with a `context` for the screen and any state that explains the render, such as `isOffline`. This makes the case queryable under the `skeleton.` namespace.
16 changes: 2 additions & 14 deletions contributingGuides/OBSERVABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,6 @@ Minimum set of parameters required to create a span:

**Span configuration**: Set of parameters passed to `Sentry.startInteractiveSpan`. See the [Sentry docs](https://docs.sentry.io/platforms/react-native/tracing/instrumentation/custom-instrumentation/#starting-inactive-spans-startinactivespan) for more details.

Additional parameters can be added as a config object (third parameter):

**Minimum Duration**: what's the minimum duration of a span. Spans shorter than this duration are discarded.

```typescript
startSpan(CONST.TELEMETRY.SPAN_SKELETON, {
name: CONST.TELEMETRY.SPAN_SKELETON,
op: CONST.TELEMETRY.SPAN_SKELETON,
}, {minDuration: CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION});
```

#### Finishing a Span

There are two ways to finish a span:
Expand All @@ -72,7 +61,7 @@ Defined in `src/CONST/index.ts` under `CONST.TELEMETRY`:
- Span names: `SPAN_OPEN_REPORT`, `SPAN_SEND_MESSAGE`
- Tag names: `TAGS.ACTIVE_POLICY`, `TAGS.AUTHENTICATION_ERROR_TYPE`
- Attribute names: `ATTRIBUTE_REPORT_ID`, `ATTRIBUTE_MESSAGE_LENGTH`
- Configuration: `CONFIG.SKELETON_MIN_DURATION`
- Configuration: `CONFIG.MEMORY_TRACKING_INTERVAL`

#### Naming Conventions

Expand All @@ -86,7 +75,7 @@ Defined in `src/CONST/index.ts` under `CONST.TELEMETRY`:
### Middleware

Process events before sending to Sentry:
- **minDurationFilter** - Discards spans shorter than a specified duration
- **maxDurationFilter** - Discards spans longer than a specified duration
- **scopeTagsEnricher** - Adds cohort and policy tags
- **emailDomainFilter** - Removes accounts we don't want to send telemetry for

Expand Down Expand Up @@ -115,7 +104,6 @@ Error conditions tracked for trend analysis:

- **ANRs**: number of "Application Not Responding" errors
- **404 pages**: number of user actions other than deep links that result in 404
- **Infinite skeletons**: Skeleton visible 10+ seconds
- **Authentication failures**: number of authentication errors other than wrong credentials

### Feature Health
Expand Down
11 changes: 0 additions & 11 deletions contributingGuides/OBSERVABILITY_METRICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,17 +172,6 @@ This document lists all implemented telemetry metrics in the Expensify App.
**End**: Immediately after start (tracking occurrence, not duration) ([`src/libs/telemetry/useAbsentPageSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useAbsentPageSpan.ts#L39))
**Attributes**: `url`, `navigationSource: 'deeplink' | 'button'`

### Infinite Skeletons

**Constant**: `CONST.TELEMETRY.SPAN_SKELETON`
**Sentry Name**: `ManualSkeleton`
**Threshold**: 10s minimum duration (only sent if visible 10+ seconds)
**What's Measured**: Number of skeleton components visible longer than expected
**Start**: Skeleton component mounted ([`src/libs/telemetry/useSkeletonSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useSkeletonSpan.ts#L13))
**End**: Component unmounts ([`src/libs/telemetry/useSkeletonSpan.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/libs/telemetry/useSkeletonSpan.ts#L24))
**Span ID**: `${CONST.TELEMETRY.SPAN_SKELETON}_${component}_${reactId}`
**Minimum Duration**: `CONST.TELEMETRY.CONFIG.SKELETON_MIN_DURATION` (10s)

### Authentication Failures

**Constants**: `CONST.TELEMETRY.TAGS.AUTHENTICATION_FUNCTION`, `CONST.TELEMETRY.TAGS.AUTHENTICATION_ERROR_TYPE`, `CONST.TELEMETRY.TAGS.AUTHENTICATION_JSON_CODE` ([`src/CONST/index.ts`](https://github.com/Expensify/App/blob/8f123f449f1a4533830b18a1040c9a5f1949821d/src/CONST/index.ts#L1700-L1702))
Expand Down
6 changes: 0 additions & 6 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2211,7 +2211,6 @@ const CONST = {
SPAN_GEOLOCATION_WAIT: 'ManualGeolocationWait',
SPAN_SEND_MESSAGE: 'ManualSendMessage',
SPAN_NOT_FOUND_PAGE: 'ManualNotFoundPage',
SPAN_SKELETON: 'ManualSkeleton',
SPAN_ODOMETER_TO_CONFIRMATION: 'ManualOdometerToConfirmation',
SPAN_ODOMETER_IMAGE_STITCH: 'ManualOdometerImageStitch',
SPAN_ODOMETER_IMAGE_CAPTURE: 'ManualOdometerImageCapture',
Expand Down Expand Up @@ -2255,7 +2254,6 @@ const CONST = {
ATTRIBUTE_CANCELED_BY_SKELETON: 'canceled_by_skeleton',
ATTRIBUTE_ROUTE_FROM: 'route_from',
ATTRIBUTE_ROUTE_TO: 'route_to',
ATTRIBUTE_MIN_DURATION: 'min_duration',
ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually',
ATTRIBUTE_IS_WARM: 'is_warm',
ATTRIBUTE_LAZY_TAB_FALLBACK_SHOWN: 'lazy_tab_fallback_shown',
Expand All @@ -2266,7 +2264,6 @@ const CONST = {
// report list, so durations that include the openApp wait can be excluded from render measurements.
ATTRIBUTE_SKELETON_SHOWN: 'skeleton_shown',
ATTRIBUTE_WAS_LIST_EMPTY: 'was_list_empty',
ATTRIBUTE_SKELETON_PREFIX: 'skeleton.',
ATTRIBUTE_SCENARIO: 'scenario',
// Start type stamped on the navigate-to-reports spans: cold, warm_first, or warm_subsequent.
ATTRIBUTE_START_TYPE: 'start_type',
Expand Down Expand Up @@ -2381,10 +2378,7 @@ const CONST = {
WARM_SUBSEQUENT: 'warm_subsequent',
UNKNOWN: 'unknown',
},
// Event names
EVENT_SKELETON_ATTRIBUTES_UPDATE: 'skeleton_attributes_updated',
CONFIG: {
SKELETON_MIN_DURATION: 10_000,
MEMORY_TRACKING_INTERVAL: 2 * 60 * 1000,

// Web Memory Thresholds (% of jsHeapSizeLimit)
Expand Down
9 changes: 1 addition & 8 deletions src/components/AccountSwitcherSkeletonView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan';

import CONST from '@src/CONST';

import type {StyleProp, ViewStyle} from 'react-native';
Expand All @@ -29,16 +26,12 @@ type AccountSwitcherSkeletonViewProps = {

/** Additional styles for the skeleton view */
style?: StyleProp<ViewStyle>;

/** Reason attributes for skeleton span telemetry */
reasonAttributes: SkeletonSpanReasonAttributes;
};

function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.DEFAULT, width, style, reasonAttributes}: AccountSwitcherSkeletonViewProps) {
function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.DEFAULT, width, style}: AccountSwitcherSkeletonViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
useSkeletonSpan('AccountSwitcherSkeletonView', reasonAttributes);
const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize);
const avatarPlaceholderRadius = avatarPlaceholderSize / 2;
const startPositionX = avatarPlaceholderRadius;
Expand Down
8 changes: 1 addition & 7 deletions src/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import useTheme from '@hooks/useTheme';

import logAppStateOnLongLoading from '@libs/AppState';
import type {ExtraLoadingContext} from '@libs/AppState';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan';

import CONST from '@src/CONST';

Expand All @@ -22,14 +20,10 @@ type ActivityIndicatorProps = RNActivityIndicatorProps & {

/** Extra loading context to be passed to the logAppStateOnLongLoading function */
extraLoadingContext?: ExtraLoadingContext;

/** Reason attributes for skeleton span telemetry */
reasonAttributes: SkeletonSpanReasonAttributes;
};

function ActivityIndicator({timeout = CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT, extraLoadingContext, reasonAttributes, ...rest}: ActivityIndicatorProps) {
function ActivityIndicator({timeout = CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT, extraLoadingContext, ...rest}: ActivityIndicatorProps) {
const theme = useTheme();
useSkeletonSpan('ActivityIndicator', reasonAttributes);

useEffect(() => {
const timeoutId = setTimeout(() => {
Expand Down
7 changes: 1 addition & 6 deletions src/components/AddPlaidBankAccount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {handlePlaidError, openPlaidBankAccountSelector, openPlaidBankLogin, setPlaidEvent} from '@libs/actions/BankAccounts';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import Log from '@libs/Log';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';

import {handleRestrictedEvent} from '@userActions/App';

Expand Down Expand Up @@ -249,13 +248,9 @@ function AddPlaidBankAccount({
}

if (plaidData?.isLoading) {
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'AddPlaidBankAccount', isLoading: !!plaidData.isLoading};
return (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={reasonAttributes}
/>
<ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />
</View>
);
}
Expand Down
9 changes: 1 addition & 8 deletions src/components/AddToWalletButton/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getPaymentMethods} from '@libs/actions/PaymentMethods';
import getPlatform from '@libs/getPlatform';
import Log from '@libs/Log';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import {checkIfWalletIsAvailable, handleAddCardToWallet, isCardInWallet} from '@libs/Wallet/index';

import CONST from '@src/CONST';
Expand Down Expand Up @@ -98,13 +97,7 @@ function AddToWalletButton({card, cardHolderName, cardDescription, style}: AddTo
}

if (isLoading) {
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'AddToWalletButton', isLoading};
return (
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={reasonAttributes}
/>
);
return <ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />;
}

if (isInWallet) {
Expand Down
14 changes: 2 additions & 12 deletions src/components/AddressSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {getCommandURL} from '@libs/ApiUtils';
import getCurrentPosition from '@libs/getCurrentPosition';
import type {GeolocationErrorCodeType} from '@libs/getCurrentPosition/getCurrentPosition.types';
import {getAddressComponents, getPlaceAutocompleteTerms} from '@libs/GooglePlacesUtils';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';

import variables from '@styles/variables';

Expand Down Expand Up @@ -81,7 +80,6 @@ function AddressSearchListEmptyComponent({searchValue, onEmptyChange}: {searchVa

function AddressSearchListLoader({onLoadingChange}: {onLoadingChange: (isLoading: boolean) => void}) {
const styles = useThemeStyles();
const reasonAttributes: SkeletonSpanReasonAttributes = {context: 'AddressSearch.listLoader'};

useEffect(() => {
onLoadingChange(true);
Expand All @@ -90,7 +88,7 @@ function AddressSearchListLoader({onLoadingChange}: {onLoadingChange: (isLoading

return (
<View style={[styles.pv4]}>
<ActivityIndicator reasonAttributes={reasonAttributes} />
<ActivityIndicator />
</View>
);
}
Expand Down Expand Up @@ -392,11 +390,6 @@ function AddressSearch({

const listLoader = useMemo(() => <AddressSearchListLoader onLoadingChange={setIsLoadingResults} />, []);

const fetchingLocationReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'AddressSearch.isFetchingCurrentLocation',
isFetchingCurrentLocation,
};

return (
/*
* The GooglePlacesAutocomplete component uses a VirtualizedList internally,
Expand Down Expand Up @@ -538,10 +531,7 @@ function AddressSearch({
</ScrollView>
{isFetchingCurrentLocation && (
<View style={[StyleSheet.absoluteFill, styles.fullScreenLoading, styles.w100]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={fetchingLocationReasonAttributes}
/>
<ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />
</View>
)}
</>
Expand Down
5 changes: 1 addition & 4 deletions src/components/Attachments/AttachmentCarousel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,7 @@ function AttachmentCarousel({
if (page == null) {
return (
<View style={[styles.flex1, styles.attachmentCarouselContainer, styles.fullScreenLoading]}>
<ActivityIndicator
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={{context: 'AttachmentCarousel'}}
/>
<ActivityIndicator size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE} />
</View>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';

import type IconAsset from '@src/types/utils/IconAsset';

import type {StyleProp, ViewStyle} from 'react-native';
Expand Down Expand Up @@ -44,11 +42,6 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
const reasonAttributes: SkeletonSpanReasonAttributes = {
context: 'DefaultAttachmentView',
shouldShowLoadingSpinnerIcon,
isUploading,
};

return (
<View style={[styles.defaultAttachmentView, containerStyles]}>
Expand Down Expand Up @@ -76,7 +69,6 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
<ActivityIndicator
color={theme.textSupporting}
testID="attachment-loading-spinner"
reasonAttributes={reasonAttributes}
/>
</Tooltip>
</View>
Expand Down
13 changes: 1 addition & 12 deletions src/components/AvatarCropModal/AvatarCropView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ import useThemeStyles from '@hooks/useThemeStyles';

import cropOrRotateImage from '@libs/cropOrRotateImage';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';

import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';

import type {LayoutChangeEvent} from 'react-native';
import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler';

import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {View} from 'react-native';
import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler';
import ImageSize from 'react-native-image-size';
Expand Down Expand Up @@ -333,15 +332,6 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
updateImageOffset(newX, newY);
};

const reasonAttributes = useMemo<SkeletonSpanReasonAttributes>(
() => ({
context: 'AvatarCropModal',
isImageInitialized,
isImageContainerInitialized,
}),
[isImageInitialized, isImageContainerInitialized],
);

return (
<ScreenWrapper
style={styles.pb0}
Expand All @@ -364,7 +354,6 @@ function AvatarCropView({imageUri = '', imageName = '', imageType = '', onClose,
<ActivityIndicator
style={styles.flex1}
size={CONST.ACTIVITY_INDICATOR_SIZE.LARGE}
reasonAttributes={reasonAttributes}
/>
) : (
<>
Expand Down
7 changes: 1 addition & 6 deletions src/components/AvatarSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';

import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import useSkeletonSpan from '@libs/telemetry/useSkeletonSpan';

import CONST from '@src/CONST';

import type {ValueOf} from 'type-fest';
Expand All @@ -15,12 +12,10 @@ import SkeletonViewContentLoader from './SkeletonViewContentLoader';

type AvatarSkeletonProps = {
size?: ValueOf<typeof CONST.AVATAR_SIZE>;
reasonAttributes: SkeletonSpanReasonAttributes;
};

function AvatarSkeleton({size = CONST.AVATAR_SIZE.SMALL, reasonAttributes}: AvatarSkeletonProps) {
function AvatarSkeleton({size = CONST.AVATAR_SIZE.SMALL}: AvatarSkeletonProps) {
const theme = useTheme();
useSkeletonSpan('AvatarSkeleton', reasonAttributes);
const StyleUtils = useStyleUtils();
const avatarSize = StyleUtils.getAvatarSize(size);
const skeletonCircleRadius = avatarSize / 2;
Expand Down
2 changes: 1 addition & 1 deletion src/components/AvatarWithIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function AvatarWithIndicator({source, accountID, tooltipText = '', fallbackIcon,
<Tooltip text={tooltipText}>
<View style={[styles.sidebarAvatar]}>
{isLoading ? (
<AvatarSkeleton reasonAttributes={{context: 'AvatarWithIndicator', isLoading}} />
<AvatarSkeleton />
) : (
<>
<Avatar
Expand Down
Loading
Loading