@segment/analytics-react-native
Version:
The hassle-free way to add Segment analytics to your React-Native app.
223 lines (199 loc) • 6.2 kB
text/typescript
import { ErrorClassification } from './types';
/**
* Error types reported through the errorHandler in the client
*/
export enum ErrorType {
NetworkUnexpectedHTTPCode,
NetworkServerLimited,
NetworkServerRejected,
NetworkUnknown,
JsonUnableToSerialize,
JsonUnableToDeserialize,
JsonUnknown,
PluginError,
InitializationError,
ResetError,
FlushError,
EventsDropped,
}
/**
* Segment Error object for ErrorHandler option
*/
export class SegmentError extends Error {
type: ErrorType;
message: string;
innerError?: unknown;
metadata?: Record<string, unknown>;
constructor(
type: ErrorType,
message: string,
innerError?: unknown,
metadata?: Record<string, unknown>
) {
super(message);
Object.setPrototypeOf(this, SegmentError.prototype);
this.type = type;
this.message = message;
this.innerError = innerError;
this.metadata = metadata;
}
}
/**
* Custom Error type for Segment HTTP Error responses
*/
export class NetworkError extends SegmentError {
statusCode: number;
type:
| ErrorType.NetworkServerLimited
| ErrorType.NetworkServerRejected
| ErrorType.NetworkUnexpectedHTTPCode
| ErrorType.NetworkUnknown;
constructor(statusCode: number, message: string, innerError?: unknown) {
let type: ErrorType;
if (statusCode === 429) {
type = ErrorType.NetworkServerLimited;
} else if (statusCode > 300 && statusCode < 400) {
type = ErrorType.NetworkUnexpectedHTTPCode;
} else if (statusCode >= 400) {
type = ErrorType.NetworkServerRejected;
} else {
type = ErrorType.NetworkUnknown;
}
super(type, message, innerError);
Object.setPrototypeOf(this, NetworkError.prototype);
this.statusCode = statusCode;
this.type = type;
}
}
/**
* Error type for JSON Serialization errors
*/
export class JSONError extends SegmentError {
constructor(
type: ErrorType.JsonUnableToDeserialize | ErrorType.JsonUnableToSerialize,
message: string,
innerError?: unknown
) {
super(type, message, innerError);
Object.setPrototypeOf(this, JSONError.prototype);
}
}
/**
* Utility method for handling HTTP fetch errors
* @param response Fetch Response
* @returns response if status OK, throws NetworkError for everything else
*/
export const checkResponseForErrors = (response: Response) => {
if (!response.ok) {
throw new NetworkError(response.status, response.statusText);
}
return response;
};
/**
* Converts a .fetch() error to a SegmentError object for reporting to the error handler
* @param error any JS error instance
* @returns a SegmentError object
*/
export const translateHTTPError = (error: unknown): SegmentError => {
// SegmentError already
if (error instanceof SegmentError) {
return error;
// JSON Deserialization Errors
} else if (error instanceof SyntaxError) {
return new JSONError(
ErrorType.JsonUnableToDeserialize,
error.message,
error
);
// HTTP Errors
} else {
const message =
error instanceof Error
? error.message
: typeof error === 'string'
? error
: 'Unknown error';
return new NetworkError(-1, message, error);
}
};
/**
* Classify an HTTP status code into rate_limit, transient, or permanent.
*
* Precedence:
* 1. statusCodeOverrides — explicit per-code overrides
* 2. 429 — rate limiting (if rateLimitEnabled !== false)
* 3. default4xxBehavior / default5xxBehavior — range defaults
* 4. Fallback — permanent (non-retryable)
*/
export const classifyError = (
statusCode: number,
config?: {
default4xxBehavior?: 'drop' | 'retry';
default5xxBehavior?: 'drop' | 'retry';
statusCodeOverrides?: Record<string, 'drop' | 'retry'>;
rateLimitEnabled?: boolean;
backoffEnabled?: boolean;
}
): ErrorClassification => {
const override = config?.statusCodeOverrides?.[statusCode.toString()];
if (override !== undefined) {
if (override === 'retry') {
// If the relevant config is disabled, treat retry overrides as permanent
if (statusCode === 429 && config?.rateLimitEnabled === false) {
return new ErrorClassification('permanent');
}
if (statusCode !== 429 && config?.backoffEnabled === false) {
return new ErrorClassification('permanent');
}
return statusCode === 429
? new ErrorClassification('rate_limit')
: new ErrorClassification('transient');
}
return new ErrorClassification('permanent');
}
if (statusCode === 429) {
return config?.rateLimitEnabled !== false
? new ErrorClassification('rate_limit')
: new ErrorClassification('permanent');
}
if (statusCode >= 400 && statusCode < 500) {
const behavior = config?.default4xxBehavior ?? 'drop';
if (behavior === 'retry' && config?.backoffEnabled === false) {
return new ErrorClassification('permanent');
}
return new ErrorClassification(
behavior === 'retry' ? 'transient' : 'permanent'
);
}
if (statusCode >= 500 && statusCode < 600) {
const behavior = config?.default5xxBehavior ?? 'retry';
if (behavior === 'retry' && config?.backoffEnabled === false) {
return new ErrorClassification('permanent');
}
return new ErrorClassification(
behavior === 'retry' ? 'transient' : 'permanent'
);
}
return new ErrorClassification('permanent');
};
/**
* Parse Retry-After header value (seconds or HTTP-date format).
* Returns delay in seconds clamped to maxRetryInterval, or undefined if invalid.
*/
export const parseRetryAfter = (
retryAfterValue: string | null,
maxRetryInterval = 300
): number | undefined => {
if (retryAfterValue === null || retryAfterValue === '') return undefined;
const asNumber = Number(retryAfterValue);
if (Number.isFinite(asNumber)) {
if (asNumber < 0) return undefined;
return Math.min(asNumber, maxRetryInterval);
}
const retryDate = new Date(retryAfterValue);
if (!isNaN(retryDate.getTime())) {
const secondsUntil = Math.ceil((retryDate.getTime() - Date.now()) / 1000);
return Math.min(Math.max(secondsUntil, 0), maxRetryInterval);
}
return undefined;
};