feat: tambah error logger ke monitoring dashboard dengan offline queue #39
@@ -85,6 +85,8 @@ export default {
|
|||||||
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
|
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
|
||||||
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
|
||||||
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
|
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
|
||||||
|
URL_MONITORING: process.env.URL_MONITORING,
|
||||||
|
KEY_API_MONITORING: process.env.KEY_API_MONITORING,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ import AuthProvider from '@/providers/AuthProvider';
|
|||||||
import ThemeProvider, { useTheme } from '@/providers/ThemeProvider';
|
import ThemeProvider, { useTheme } from '@/providers/ThemeProvider';
|
||||||
import QueryProvider from '@/providers/QueryProvider';
|
import QueryProvider from '@/providers/QueryProvider';
|
||||||
import ErrorBoundary from '@/components/ErrorBoundary';
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
||||||
|
import { flushErrorQueue } from '@/lib/errorLogger';
|
||||||
import { useFonts } from 'expo-font';
|
import { useFonts } from 'expo-font';
|
||||||
import { Stack } from 'expo-router';
|
import { Stack } from 'expo-router';
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { AppState } from 'react-native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { NotifierWrapper } from 'react-native-notifier';
|
import { NotifierWrapper } from 'react-native-notifier';
|
||||||
import 'react-native-reanimated';
|
import 'react-native-reanimated';
|
||||||
@@ -38,9 +40,17 @@ export default function RootLayout() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (loaded) {
|
if (loaded) {
|
||||||
SplashScreen.hideAsync();
|
SplashScreen.hideAsync();
|
||||||
|
flushErrorQueue();
|
||||||
}
|
}
|
||||||
}, [loaded]);
|
}, [loaded]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sub = AppState.addEventListener('change', (state) => {
|
||||||
|
if (state === 'active') flushErrorQueue();
|
||||||
|
});
|
||||||
|
return () => sub.remove();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export default class ErrorBoundary extends Component<Props, State> {
|
|||||||
return { hasError: true, error };
|
return { hasError: true, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error): void {
|
||||||
|
const { logError } = require('@/lib/errorLogger');
|
||||||
|
logError(error.message, error);
|
||||||
|
}
|
||||||
|
|
||||||
handleReset = () => {
|
handleReset = () => {
|
||||||
this.setState({ hasError: false, error: null });
|
this.setState({ hasError: false, error: null });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import Constants from 'expo-constants';
|
|||||||
export const ConstEnv = {
|
export const ConstEnv = {
|
||||||
url_storage: Constants?.expoConfig?.extra?.URL_STORAGE,
|
url_storage: Constants?.expoConfig?.extra?.URL_STORAGE,
|
||||||
pass_encrypt: Constants?.expoConfig?.extra?.PASS_ENC,
|
pass_encrypt: Constants?.expoConfig?.extra?.PASS_ENC,
|
||||||
|
url_monitoring: Constants?.expoConfig?.extra?.URL_MONITORING,
|
||||||
|
key_api_monitoring: Constants?.expoConfig?.extra?.KEY_API_MONITORING,
|
||||||
firebase: {
|
firebase: {
|
||||||
apiKey: Constants?.expoConfig?.extra?.FIREBASE_API_KEY,
|
apiKey: Constants?.expoConfig?.extra?.FIREBASE_API_KEY,
|
||||||
authDomain: Constants?.expoConfig?.extra?.FIREBASE_AUTH_DOMAIN,
|
authDomain: Constants?.expoConfig?.extra?.FIREBASE_AUTH_DOMAIN,
|
||||||
|
|||||||
14
lib/api.ts
14
lib/api.ts
@@ -1,10 +1,22 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
import Constants from 'expo-constants';
|
import Constants from 'expo-constants';
|
||||||
|
import { logError } from '@/lib/errorLogger';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: Constants?.expoConfig?.extra?.URL_API
|
baseURL: Constants?.expoConfig?.extra?.URL_API
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error: AxiosError) => {
|
||||||
|
const status = error.response?.status;
|
||||||
|
const url = error.config?.url ?? 'unknown endpoint';
|
||||||
|
const description = `API error ${status ?? 'network'} on ${url}`;
|
||||||
|
logError(description, error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const apiCheckPhoneLogin = async (body: { phone: string }) => {
|
export const apiCheckPhoneLogin = async (body: { phone: string }) => {
|
||||||
const response = await api.post('/auth/login', body)
|
const response = await api.post('/auth/login', body)
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
100
lib/errorLogger.ts
Normal file
100
lib/errorLogger.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import NetInfo from '@react-native-community/netinfo';
|
||||||
|
import Constants from 'expo-constants';
|
||||||
|
import * as Device from 'expo-device';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
|
import { ConstEnv } from '@/constants/ConstEnv';
|
||||||
|
|
||||||
|
const QUEUE_KEY = 'error_log_queue';
|
||||||
|
const APP_NAME = 'desa-plus';
|
||||||
|
|
||||||
|
type ErrorPayload = {
|
||||||
|
affectedVersion: string;
|
||||||
|
device: string;
|
||||||
|
os: string;
|
||||||
|
description: string;
|
||||||
|
app: string;
|
||||||
|
source: 'SYSTEM';
|
||||||
|
stackTrace: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildPayload(description: string, stackTrace?: string): ErrorPayload {
|
||||||
|
const platformName = Platform.OS === 'ios' ? 'iOS' : 'Android';
|
||||||
|
return {
|
||||||
|
affectedVersion: Constants.expoConfig?.version ?? 'unknown',
|
||||||
|
device: Device.modelName ?? 'unknown',
|
||||||
|
os: `${platformName} ${Device.osVersion ?? ''}`.trim(),
|
||||||
|
description,
|
||||||
|
app: APP_NAME,
|
||||||
|
source: 'SYSTEM',
|
||||||
|
stackTrace: stackTrace ?? '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendToMonitoring(payload: ErrorPayload): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${ConstEnv.url_monitoring}/api/bugs`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-Key': ConstEnv.key_api_monitoring ?? '',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
return res.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function enqueue(payload: ErrorPayload): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await AsyncStorage.getItem(QUEUE_KEY);
|
||||||
|
const queue: ErrorPayload[] = raw ? JSON.parse(raw) : [];
|
||||||
|
queue.push(payload);
|
||||||
|
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushErrorQueue(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const state = await NetInfo.fetch();
|
||||||
|
if (!state.isConnected) return;
|
||||||
|
|
||||||
|
const raw = await AsyncStorage.getItem(QUEUE_KEY);
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
const queue: ErrorPayload[] = JSON.parse(raw);
|
||||||
|
if (queue.length === 0) return;
|
||||||
|
|
||||||
|
const failed: ErrorPayload[] = [];
|
||||||
|
for (const payload of queue) {
|
||||||
|
const ok = await sendToMonitoring(payload);
|
||||||
|
if (!ok) failed.push(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failed.length > 0) {
|
||||||
|
await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(failed));
|
||||||
|
} else {
|
||||||
|
await AsyncStorage.removeItem(QUEUE_KEY);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logError(description: string, error?: Error | unknown): Promise<void> {
|
||||||
|
const stackTrace = error instanceof Error ? (error.stack ?? error.message) : String(error ?? '');
|
||||||
|
const payload = buildPayload(description, stackTrace);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const state = await NetInfo.fetch();
|
||||||
|
if (state.isConnected) {
|
||||||
|
const ok = await sendToMonitoring(payload);
|
||||||
|
if (!ok) await enqueue(payload);
|
||||||
|
} else {
|
||||||
|
await enqueue(payload);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
await enqueue(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user