Last active
August 9, 2023 10:00
-
-
Save harrisrobin/c81445076242e0400a6cdf716a60e098 to your computer and use it in GitHub Desktop.
Typed & re-useable analytics module to make it easy to add new analytics tools to the app without having to touch multiple files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as Analytics from "expo-firebase-analytics" | |
import { recordError } from "./exceptions" | |
export const setCurrentScreen = (screenName: string): void => { | |
void Analytics.logEvent("screen_view", { screen_name: screenName }) | |
// track screens. | |
__DEV__ && console.tron.log(`currentScreen: ${screenName}`) | |
} | |
/** | |
* Logs an event to various analytics services. | |
*/ | |
export async function logEvent( | |
event: string, | |
params: Record<string, unknown> = {}, | |
): Promise<void> { | |
__DEV__ && console.tron.log("logEvent", { event, ...params }) | |
try { | |
await Promise.all([Analytics.logEvent(event, params)]) | |
} catch (error) { | |
console.error("Something with wrong with Analytics.logEvent", error) | |
recordError(error) | |
} | |
} | |
/** | |
* Sets the user ID property. This feature must be used in accordance with Google's Privacy Policy | |
*/ | |
export async function setCurrentUserId(userId: string): Promise<void> { | |
__DEV__ && console.tron.log("setCurrentUserId", userId) | |
try { | |
await Analytics.setUserId(userId) | |
} catch (error) { | |
console.error("Something with wrong with Analytics.setCurrentUserId", error) | |
recordError(error) | |
} | |
} | |
/** | |
* Clears all analytics data for this instance from the device and resets the app instance ID. | |
*/ | |
export async function flush(): Promise<void> { | |
try { | |
await Analytics.resetAnalyticsData() | |
} catch (error) { | |
console.error("Something with wrong with Analytics.flush", error) | |
recordError(error) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Alert } from "react-native" | |
import { setJSExceptionHandler } from "react-native-exception-handler" | |
import { NAVIGATION_PERSISTENCE_KEY } from "../navigation/navigation-utilities" | |
import { translate } from "@bridge/i18n" | |
import { ROOT_STATE_STORAGE_KEY } from "../models/root-store/constants" | |
import * as storage from "./storage" | |
/** | |
* Error classifications used to sort errors on error reporting services. | |
*/ | |
export enum ErrorType { | |
/** | |
* An API service related error. | |
*/ | |
API, | |
/** | |
* An error that would normally cause a red screen in dev | |
* and forced the user to signout and restart. | |
*/ | |
FATAL, | |
/** | |
* An error caught by try/catch where defined using Reactotron.tron.error. | |
*/ | |
HANDLED, | |
} | |
/** | |
* Manually record a handled error. | |
*/ | |
export const recordError = ( | |
error: any, | |
type: ErrorType = ErrorType.FATAL, | |
): void => { | |
const message = error.message || "Unknown" | |
console.error(error) | |
console.log(message, type) | |
} | |
/** | |
* The exception handler | |
*/ | |
const exceptionHandler = (error: Error, isFatal: boolean) => { | |
// why? what did I do? | |
const receivedError = error instanceof Error ? error : JSON.stringify(error) | |
if (isFatal) { | |
recordError(receivedError, ErrorType.FATAL) | |
// let the user know we dun goofed | |
Alert.alert( | |
translate("alerts.errors.generic"), | |
`${translate("alerts.errors.ourTeam")} ${error.message}`, | |
[ | |
{ | |
text: translate("common.ok"), | |
onPress: async () => { | |
await Promise.all([ | |
storage.remove(ROOT_STATE_STORAGE_KEY), | |
storage.remove(NAVIGATION_PERSISTENCE_KEY), | |
]) | |
}, | |
}, | |
], | |
) | |
} | |
} | |
/** | |
* Handle uncaught exceptions | |
*/ | |
export const handleExceptions = (allowInDev = true): void => { | |
setJSExceptionHandler(exceptionHandler, allowInDev) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AsyncStorage } from "./async-storage" | |
import { load, loadString, save, saveString, clear, remove } from "./storage" | |
// fixtures | |
const VALUE_OBJECT = { x: 1 } | |
const VALUE_STRING = JSON.stringify(VALUE_OBJECT) | |
beforeEach(() => | |
(AsyncStorage.getItem as jest.Mock).mockReturnValue( | |
Promise.resolve(VALUE_STRING), | |
), | |
) | |
afterEach(() => jest.clearAllMocks()) | |
test("load", async () => { | |
const value = await load("something") | |
expect(value).toEqual(JSON.parse(VALUE_STRING)) | |
}) | |
test("loadString", async () => { | |
const value = await loadString("something") | |
expect(value).toEqual(VALUE_STRING) | |
}) | |
test("save", async () => { | |
await save("something", VALUE_OBJECT) | |
expect(AsyncStorage.setItem).toHaveBeenCalledWith("something", VALUE_STRING) | |
}) | |
test("saveString", async () => { | |
await saveString("something", VALUE_STRING) | |
expect(AsyncStorage.setItem).toHaveBeenCalledWith("something", VALUE_STRING) | |
}) | |
test("remove", async () => { | |
await remove("something") | |
expect(AsyncStorage.removeItem).toHaveBeenCalledWith("something") | |
}) | |
test("clear", async () => { | |
await clear() | |
expect(AsyncStorage.clear).toHaveBeenCalledWith() | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-empty */ | |
import { AsyncStorage } from "./async-storage" | |
/** | |
* Loads a string from storage. | |
* | |
* @param key The key to fetch. | |
*/ | |
export async function loadString(key: string): Promise<string | null> { | |
try { | |
return await AsyncStorage.getItem(key) | |
} catch { | |
// not sure why this would fail... even reading the RN docs I'm unclear | |
return null | |
} | |
} | |
/** | |
* Saves a string to storage. | |
* | |
* @param key The key to fetch. | |
* @param value The value to store. | |
*/ | |
export async function saveString(key: string, value: string): Promise<boolean> { | |
try { | |
await AsyncStorage.setItem(key, value) | |
return true | |
} catch { | |
return false | |
} | |
} | |
/** | |
* Loads something from storage and runs it thru JSON.parse. | |
* | |
* @param key The key to fetch. | |
*/ | |
export async function load(key: string): Promise<any | null> { | |
try { | |
const almostThere = await AsyncStorage.getItem(key) | |
//@ts-expect-error | |
return JSON.parse(almostThere) | |
} catch { | |
return null | |
} | |
} | |
/** | |
* Saves an object to storage. | |
* | |
* @param key The key to fetch. | |
* @param value The value to store. | |
*/ | |
export async function save(key: string, value: any): Promise<boolean> { | |
try { | |
await AsyncStorage.setItem(key, JSON.stringify(value)) | |
return true | |
} catch { | |
return false | |
} | |
} | |
/** | |
* Removes something from storage. | |
* | |
* @param key The key to kill. | |
*/ | |
export async function remove(key: string): Promise<void> { | |
try { | |
await AsyncStorage.removeItem(key) | |
} catch {} | |
} | |
/** | |
* Burn it all to the ground. | |
*/ | |
export async function clear(): Promise<void> { | |
try { | |
await AsyncStorage.clear() | |
} catch {} | |
} | |
/** | |
* Check if key exists in storage. | |
*/ | |
export async function hasKey(key: string): Promise<boolean> { | |
try { | |
const item = await AsyncStorage.getItem(key) | |
return item != null | |
} catch { | |
return false | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment