diff --git a/packages/app/lib/internal/index.ts b/packages/app/lib/internal/index.ts index 4774df70c0..cc172b959d 100644 --- a/packages/app/lib/internal/index.ts +++ b/packages/app/lib/internal/index.ts @@ -25,3 +25,4 @@ export * from './registry/namespace'; export * from './registry/nativeModule'; export { default as SharedEventEmitter } from './SharedEventEmitter'; export { Logger } from './logger'; +export type { ModuleConfig } from '../types/internal'; diff --git a/packages/crashlytics/lib/handlers.js b/packages/crashlytics/lib/handlers.ts similarity index 77% rename from packages/crashlytics/lib/handlers.js rename to packages/crashlytics/lib/handlers.ts index 55021113fa..6a6d56d3bf 100644 --- a/packages/crashlytics/lib/handlers.js +++ b/packages/crashlytics/lib/handlers.ts @@ -15,20 +15,48 @@ * */ -import { firebase } from '@react-native-firebase/app'; +import { firebase } from '.'; import { isError, once } from '@react-native-firebase/app/lib/common'; +// @ts-ignore - No declaration file for promise/setimmediate/rejection-tracking import tracking from 'promise/setimmediate/rejection-tracking'; import StackTrace from 'stacktrace-js'; - export const FATAL_FLAG = 'com.firebase.crashlytics.reactnative.fatal'; -export function createNativeErrorObj(error, stackFrames, isUnhandledRejection, jsErrorName) { - const nativeObj = {}; +interface NativeErrorFrame { + src: string; + line: number; + col: number; + fn: string; + file: string; +} + +interface NativeErrorObj { + message: string; + isUnhandledRejection: boolean; + frames: NativeErrorFrame[]; +} - nativeObj.message = `${error.message}`; - nativeObj.isUnhandledRejection = isUnhandledRejection; +interface NativeModule { + isCrashlyticsCollectionEnabled: boolean; + isErrorGenerationOnJSCrashEnabled: boolean; + isCrashlyticsJavascriptExceptionHandlerChainingEnabled: boolean; + logPromise(message: string): Promise; + setAttribute(name: string, value: string): Promise; + recordErrorPromise(errorObj: NativeErrorObj): Promise; + crashWithStackPromise(errorObj: NativeErrorObj): Promise; +} - nativeObj.frames = []; +export function createNativeErrorObj( + error: Error, + stackFrames: StackTrace.StackFrame[], + isUnhandledRejection: boolean, + jsErrorName?: string, +): NativeErrorObj { + const nativeObj: NativeErrorObj = { + message: `${error.message}`, + isUnhandledRejection, + frames: [], + }; if (jsErrorName) { // Option to fix crashlytics display and alerting. You can add an error name to the recordError function @@ -42,7 +70,10 @@ export function createNativeErrorObj(error, stackFrames, isUnhandledRejection, j } for (let i = 0; i < stackFrames.length; i++) { - const { columnNumber, lineNumber, fileName, functionName, source } = stackFrames[i]; + const frame = stackFrames[i]; + if (!frame) continue; + + const { columnNumber, lineNumber, fileName, functionName, source } = frame; let fileNameParsed = ''; if (fileName) { const subStrLen = fileName.indexOf('?'); @@ -54,7 +85,7 @@ export function createNativeErrorObj(error, stackFrames, isUnhandledRejection, j } nativeObj.frames.push({ - src: source, + src: source || '', line: lineNumber || 0, col: columnNumber || 0, fn: functionName || '', @@ -65,10 +96,10 @@ export function createNativeErrorObj(error, stackFrames, isUnhandledRejection, j return nativeObj; } -export const setGlobalErrorHandler = once(nativeModule => { +export const setGlobalErrorHandler = once((nativeModule: NativeModule) => { const originalHandler = ErrorUtils.getGlobalHandler(); - async function handler(error, fatal) { + async function handler(error: unknown, fatal?: boolean) { // If collection is disabled, just forward to the original handler if (!nativeModule.isCrashlyticsCollectionEnabled) { return originalHandler(error, fatal); @@ -89,7 +120,7 @@ export const setGlobalErrorHandler = once(nativeModule => { // For that reason we always round up (`.ceil`) and add a second in case of latency // // Time is specified as seconds since start of Unix epoch as a baseline, as a string - const fatalTime = Math.ceil(new Date() / 1000) + 1 + ''; + const fatalTime = Math.ceil(new Date().getTime() / 1000) + 1 + ''; // Flag the Crashlytics backend that we have a fatal error, they will transform it await nativeModule.setAttribute(FATAL_FLAG, fatalTime); @@ -107,6 +138,7 @@ export const setGlobalErrorHandler = once(nativeModule => { // Did not matter if I did named imports above or dynamic require here. // So temporarily reverting and silencing warnings instead globalThis.RNFB_SILENCE_MODULAR_DEPRECATION_WARNINGS = true; + // @ts-ignore - analytics types not available in crashlytics await firebase.app().analytics().logEvent( 'app_exception', // 'app_exception' is reserved but we make an exception for JS->fatal transforms { @@ -143,8 +175,8 @@ export const setGlobalErrorHandler = once(nativeModule => { return handler; }); -export const setOnUnhandledPromiseRejectionHandler = once(nativeModule => { - async function onUnhandled(_id, error) { +export const setOnUnhandledPromiseRejectionHandler = once((nativeModule: NativeModule) => { + async function onUnhandled(_id: number, error: Error) { if (!__DEV__) { // TODO(salakar): Option to disable try { diff --git a/packages/crashlytics/lib/index.d.ts b/packages/crashlytics/lib/index.d.ts deleted file mode 100644 index f2f0ea18e5..0000000000 --- a/packages/crashlytics/lib/index.d.ts +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (c) 2016-present Invertase Limited & Contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this library except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -import { ReactNativeFirebase } from '@react-native-firebase/app'; - -/** - * Firebase Crashlytics package for React Native. - * - * #### Example: Access the firebase export from the `crashlytics` package: - * - * ```js - * import { firebase } from '@react-native-firebase/crashlytics'; - * - * // firebase.crashlytics().X - * ``` - * - * #### Example: Using the default export from the `crashlytics` package: - * - * ```js - * import crashlytics from '@react-native-firebase/crashlytics'; - * - * // crashlytics().X - * ``` - * - * #### Example: Using the default export from the `app` package: - * - * ```js - * import firebase from '@react-native-firebase/app'; - * import '@react-native-firebase/crashlytics'; - * - * // firebase.crashlytics().X - * ``` - * - * @firebase crashlytics - */ -export namespace FirebaseCrashlyticsTypes { - import FirebaseModule = ReactNativeFirebase.FirebaseModule; - - export interface Statics { - SDK_VERSION: string; - } - - /** - * The Firebase Crashlytics service interface. - * - * > This module is available for the default app only. - * - * #### Example - * - * Get the Crashlytics service for the default app: - * - * ```js - * const defaultAppCrashlytics = firebase.crashlytics(); - * ``` - */ - export class Module extends FirebaseModule { - /** - * The current `FirebaseApp` instance for this Firebase service. - */ - app: ReactNativeFirebase.FirebaseApp; - - /** - * Whether Crashlytics reporting is enabled. - * - * #### Example - * - * ```js - * const isEnabled = firebase.crashlytics().isCrashlyticsCollectionEnabled; - * ``` - * - */ - isCrashlyticsCollectionEnabled: boolean; - /** - * Determines whether there are any unsent crash reports cached on the device. The callback only executes - * if automatic data collection is disabled. - * - * #### Example - * - * ```js - * async checkReports() { - * // returns boolean value - * const unsentReports = await firebase.crashlytics().checkForUnsentReports(); - * } - * - * checkReports(); - * ``` - * - */ - checkForUnsentReports(): Promise; - /** - * Deletes any unsent reports on the device. This method only applies if automatic data collection is - * disabled. - * - * #### Example - * - * ```js - * firebase.crashlytics().deleteUnsentReports(); - * ``` - * - */ - deleteUnsentReports(): Promise; - /** - * Returns a boolean value indicating whether the app crashed during the previous execution. - * - * #### Example - * - * ```js - * async didCrashPreviously() { - * // returns boolean value - * const didCrash = await firebase.crashlytics().didCrashOnPreviousExecution(); - * } - * - * didCrashPreviously(); - * ``` - * - */ - didCrashOnPreviousExecution(): Promise; - - /** - * Cause your app to crash for testing purposes. This is a native crash and will not contain a javascript stack trace. - * Note that crashes are intercepted by debuggers on iOS so no report will be seen under those conditions. Additionally - * if it is a debug build you will need to ensure your firebase.json is configured to enable crashlytics even in debug mode. - * - * #### Example - * - * ```js - * firebase.crashlytics().crash(); - * ``` - * - */ - crash(): void; - - /** - * Log a message that will appear in any subsequent Crash or Non-fatal error reports. - * - * #### Example - * - * ```js - * firebase.crashlytics().log('Testing a crash'); - * firebase.crashlytics().crash(); - * ``` - * - * @param message - */ - log(message: string): void; - - /** - * Record a JavaScript Error. - * - * The JavaScript stack trace is converted into a mock native iOS or Android exception before submission. - * The line numbers in the stack trace (if available) will be relative to the javascript bundle built by your packager, - * after whatever transpilation or minimization steps happen. You will need to maintain sourcemaps to decode them if desired. - * - * #### Example - * - * ```js - * firebase.crashlytics().recordError( - * new Error('An error was caught') - * ); - * ``` - * - * @param error Expects an instance of Error; e.g. classes that extend Error will also be supported. - * @param jsErrorName Optional string containing Javascript error name - */ - recordError(error: Error, jsErrorName?: string): void; - /** - * Enqueues any unsent reports on the device to upload to Crashlytics. This method only applies if - * automatic data collection is disabled. - * - * #### Example - * - * ```js - * firebase.crashlytics().sendUnsentReports(); - * ``` - */ - sendUnsentReports(): void; - - /** - * Specify a user identifier which will be visible in the Firebase Crashlytics console. - * - * It is recommended for privacy purposes that this value be a value that's meaningless to a third-party - * observer; such as an arbitrary string that ties an end-user to a record in your system e.g. a database record id. - * - * #### Example - * - * ```js - * // Custom user id - * await firebase.crashlytics().setUserId('123456789'); - * // Firebase auth uid - * await firebase.crashlytics().setUserId( - * firebase.auth().currentUser.uid - * ); - * ``` - * - * @param userId An arbitrary string that ties an end-user to a record in your system e.g. a database record id. - */ - setUserId(userId: string): Promise; - - /** - * Sets a string value to be associated with the given attribute name which will be visible in the Firebase Crashlytics console. - * - * #### Example - * - * ```js - * await firebase.crashlytics().setAttribute('role', 'admin'); - * ``` - * - * @param name The name of the attribute to set. - * @param value A string value for the given attribute. - */ - setAttribute(name: string, value: string): Promise; - - /** - * Like `setAttribute` but for multiple attributes. - * - * #### Example - * - * ```js - * await firebase.crashlytics().setAttributes({ - * role: 'admin', - * followers: '13', - * }); - * ``` - * - * @param attributes An object of key/value attribute name and values. - */ - setAttributes(attributes: { [key: string]: string }): Promise; - - /** - * Enable/disable Crashlytics reporting. - * - * Use this for opt-in first user data collection flows combined with `firebase.json` settings to disable auto collection. - * - * #### Example - * - * ```js - * // Disable crash reporting - * await firebase.crashlytics().setCrashlyticsCollectionEnabled(false); - * ``` - * - * @param enabled A boolean value representing whether to enable Crashlytics error collection. - */ - setCrashlyticsCollectionEnabled(enabled: boolean): Promise; - } -} - -type CrashlyticsNamespace = ReactNativeFirebase.FirebaseModuleWithStatics< - FirebaseCrashlyticsTypes.Module, - FirebaseCrashlyticsTypes.Statics -> & { - crashlytics: ReactNativeFirebase.FirebaseModuleWithStatics< - FirebaseCrashlyticsTypes.Module, - FirebaseCrashlyticsTypes.Statics - >; - firebase: ReactNativeFirebase.Module; - app(name?: string): ReactNativeFirebase.FirebaseApp; -}; - -declare const defaultExport: CrashlyticsNamespace; - -export const firebase: ReactNativeFirebase.Module & { - crashlytics: typeof defaultExport; - app( - name?: string, - ): ReactNativeFirebase.FirebaseApp & { crashlytics(): FirebaseCrashlyticsTypes.Module }; -}; - -export default defaultExport; - -export * from './modular'; - -/** - * Attach namespace to `firebase.` and `FirebaseApp.`. - */ -declare module '@react-native-firebase/app' { - namespace ReactNativeFirebase { - import FirebaseModuleWithStatics = ReactNativeFirebase.FirebaseModuleWithStatics; - interface Module { - crashlytics: FirebaseModuleWithStatics< - FirebaseCrashlyticsTypes.Module, - FirebaseCrashlyticsTypes.Statics - >; - } - interface FirebaseApp { - crashlytics(): FirebaseCrashlyticsTypes.Module; - } - } -} diff --git a/packages/crashlytics/lib/index.ts b/packages/crashlytics/lib/index.ts new file mode 100644 index 0000000000..3e07dbac9b --- /dev/null +++ b/packages/crashlytics/lib/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// Export types from types/crashlytics +export type { Crashlytics, FirebaseCrashlyticsTypes } from './types/crashlytics'; + +// Export modular API functions +export * from './modular'; + +// Export namespaced API +export * from './namespaced'; +export { default } from './namespaced'; diff --git a/packages/crashlytics/lib/modular/index.js b/packages/crashlytics/lib/modular.ts similarity index 62% rename from packages/crashlytics/lib/modular/index.js rename to packages/crashlytics/lib/modular.ts index 7c62383a15..1f5fa60f8f 100644 --- a/packages/crashlytics/lib/modular/index.js +++ b/packages/crashlytics/lib/modular.ts @@ -1,10 +1,6 @@ import { getApp } from '@react-native-firebase/app'; import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/lib/common'; - -/** - * @typedef {import('@firebase/app').FirebaseApp} FirebaseApp - * @typedef {import('..').FirebaseCrashlyticsTypes.Module} FirebaseCrashlytics - */ +import type { Crashlytics } from './types/crashlytics'; /** * Returns Crashlytics instance. @@ -12,10 +8,8 @@ import { MODULAR_DEPRECATION_ARG } from '@react-native-firebase/app/lib/common'; * ```js * const crashlytics = getCrashlytics(); * ``` - * @param {FirebaseApp} app - * @returns {FirebaseCrashlytics} */ -export function getCrashlytics() { +export function getCrashlytics(): Crashlytics { return getApp().crashlytics(); } @@ -34,10 +28,11 @@ export function getCrashlytics() { * * checkReports(); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @returns Promise that resolves to a boolean indicating if there are unsent reports. */ -export function checkForUnsentReports(crashlytics) { +export function checkForUnsentReports(crashlytics: Crashlytics): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.checkForUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); } @@ -51,10 +46,10 @@ export function checkForUnsentReports(crashlytics) { * const crashlytics = getCrashlytics(); * deleteUnsentReports(crashlytics); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @returns {Promise} + * @param crashlytics A crashlytics instance. */ -export function deleteUnsentReports(crashlytics) { +export function deleteUnsentReports(crashlytics: Crashlytics): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.deleteUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); } @@ -72,10 +67,11 @@ export function deleteUnsentReports(crashlytics) { * * didCrashPreviously(); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @returns Promise that resolves to a boolean indicating if the app crashed previously. */ -export function didCrashOnPreviousExecution(crashlytics) { +export function didCrashOnPreviousExecution(crashlytics: Crashlytics): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.didCrashOnPreviousExecution.call(crashlytics, MODULAR_DEPRECATION_ARG); } @@ -90,10 +86,10 @@ export function didCrashOnPreviousExecution(crashlytics) { * const crashlytics = getCrashlytics(); * crash(crashlytics); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @returns {void} + * @param crashlytics A crashlytics instance. */ -export function crash(crashlytics) { +export function crash(crashlytics: Crashlytics): void { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.crash.call(crashlytics, MODULAR_DEPRECATION_ARG); } @@ -107,11 +103,11 @@ export function crash(crashlytics) { * log(crashlytics, 'Testing a crash'); * crash(crashlytics); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {string} message - * @returns {void} + * @param crashlytics A crashlytics instance. + * @param message The message to log. */ -export function log(crashlytics, message) { +export function log(crashlytics: Crashlytics, message: string): void { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.log.call(crashlytics, message, MODULAR_DEPRECATION_ARG); } @@ -131,12 +127,12 @@ export function log(crashlytics, message) { * new Error('An error was caught') * ); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {Error} error - * @param {string | undefined} jsErrorName - * @returns {void} + * @param crashlytics A crashlytics instance. + * @param error Expects an instance of Error; e.g. classes that extend Error will also be supported. + * @param jsErrorName Optional string containing Javascript error name */ -export function recordError(crashlytics, error, jsErrorName) { +export function recordError(crashlytics: Crashlytics, error: Error, jsErrorName?: string): void { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.recordError.call(crashlytics, error, jsErrorName, MODULAR_DEPRECATION_ARG); } @@ -150,10 +146,10 @@ export function recordError(crashlytics, error, jsErrorName) { * const crashlytics = getCrashlytics(); * sendUnsentReports(crashlytics); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @returns {void} + * @param crashlytics A crashlytics instance. */ -export function sendUnsentReports(crashlytics) { +export function sendUnsentReports(crashlytics: Crashlytics): void { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.sendUnsentReports.call(crashlytics, MODULAR_DEPRECATION_ARG); } @@ -176,11 +172,11 @@ export function sendUnsentReports(crashlytics) { * auth.currentUser.uid * ); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {string} userId - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @param userId An arbitrary string that ties an end-user to a record in your system e.g. a database record id. */ -export function setUserId(crashlytics, userId) { +export function setUserId(crashlytics: Crashlytics, userId: string): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.setUserId.call(crashlytics, userId, MODULAR_DEPRECATION_ARG); } @@ -193,12 +189,12 @@ export function setUserId(crashlytics, userId) { * const crashlytics = getCrashlytics(); * await setAttribute(crashlytics, 'role', 'admin'); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {string} name - * @param {string} value - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @param name The name of the attribute to set. + * @param value A string value for the given attribute. */ -export function setAttribute(crashlytics, name, value) { +export function setAttribute(crashlytics: Crashlytics, name: string, value: string): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.setAttribute.call(crashlytics, name, value, MODULAR_DEPRECATION_ARG); } @@ -214,11 +210,14 @@ export function setAttribute(crashlytics, name, value) { * followers: '13', * }); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {{ [key: string]: string }} attributes - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @param attributes An object of key/value attribute name and values. */ -export function setAttributes(crashlytics, attributes) { +export function setAttributes( + crashlytics: Crashlytics, + attributes: { [key: string]: string }, +): Promise { + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope return crashlytics.setAttributes.call(crashlytics, attributes, MODULAR_DEPRECATION_ARG); } @@ -234,14 +233,17 @@ export function setAttributes(crashlytics, attributes) { * // Disable crash reporting * await setCrashlyticsCollectionEnabled(crashlytics, false); * ``` - * @param {FirebaseCrashlytics} crashlytics - * @param {boolean} enabled - * @returns {Promise} + * @param crashlytics A crashlytics instance. + * @param enabled A boolean value representing whether to enable Crashlytics error collection. */ -export function setCrashlyticsCollectionEnabled(crashlytics, enabled) { +export function setCrashlyticsCollectionEnabled( + crashlytics: Crashlytics, + enabled: boolean, +): Promise { return crashlytics.setCrashlyticsCollectionEnabled.call( crashlytics, enabled, + // @ts-ignore - MODULAR_DEPRECATION_ARG is not defined in the global scope MODULAR_DEPRECATION_ARG, ); } diff --git a/packages/crashlytics/lib/modular/index.d.ts b/packages/crashlytics/lib/modular/index.d.ts deleted file mode 100644 index 02d7a71e4a..0000000000 --- a/packages/crashlytics/lib/modular/index.d.ts +++ /dev/null @@ -1,196 +0,0 @@ -import { FirebaseCrashlyticsTypes } from '..'; - -type FirebaseCrashlytics = FirebaseCrashlyticsTypes.Module; - -/** - * Returns Crashlytics instance. - * #### Example - * ```js - * const crashlytics = getCrashlytics(); - * ``` - */ -export declare function getCrashlytics(): FirebaseCrashlytics; - -/** - * Determines whether there are any unsent crash reports cached on the device. The callback only executes - * if automatic data collection is disabled. - * - * #### Example - * - * ```js - * async checkReports() { - * // returns boolean value - * const crashlytics = getCrashlytics(); - * const unsentReports = await checkForUnsentReports(crashlytics); - * } - * - * checkReports(); - * ``` - */ -export declare function checkForUnsentReports(crashlytics: FirebaseCrashlytics): Promise; -/** - * Deletes any unsent reports on the device. This method only applies if automatic data collection is - * disabled. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * deleteUnsentReports(crashlytics); - * ``` - */ -export declare function deleteUnsentReports(crashlytics: FirebaseCrashlytics): Promise; -/** - * Returns a boolean value indicating whether the app crashed during the previous execution. - * - * #### Example - * - * ```js - * async didCrashPreviously() { - * // returns boolean value - * const crashlytics = getCrashlytics(); - * const didCrash = await didCrashOnPreviousExecution(crashlytics); - * } - * - * didCrashPreviously(); - * ``` - */ -export declare function didCrashOnPreviousExecution( - crashlytics: FirebaseCrashlytics, -): Promise; - -/** - * Cause your app to crash for testing purposes. This is a native crash and will not contain a javascript stack trace. - * Note that crashes are intercepted by debuggers on iOS so no report will be seen under those conditions. Additionally - * if it is a debug build you will need to ensure your firebase.json is configured to enable crashlytics even in debug mode. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * crash(crashlytics); - * ``` - */ -export declare function crash(crashlytics: FirebaseCrashlytics): void; - -/** - * Log a message that will appear in any subsequent Crash or Non-fatal error reports. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * log(crashlytics, 'Testing a crash'); - * crash(crashlytics); - * ``` - */ -export declare function log(crashlytics: FirebaseCrashlytics, message: string): void; - -/** - * Record a JavaScript Error. - * - * The JavaScript stack trace is converted into a mock native iOS or Android exception before submission. - * The line numbers in the stack trace (if available) will be relative to the javascript bundle built by your packager, - * after whatever transpilation or minimization steps happen. You will need to maintain sourcemaps to decode them if desired. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * recordError( - * crashlytics, - * new Error('An error was caught') - * ); - * ``` - */ -export declare function recordError( - crashlytics: FirebaseCrashlytics, - error: Error, - jsErrorName?: string, -): void; -/** - * Enqueues any unsent reports on the device to upload to Crashlytics. This method only applies if - * automatic data collection is disabled. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * sendUnsentReports(crashlytics); - * ``` - */ -export declare function sendUnsentReports(crashlytics: FirebaseCrashlytics): void; - -/** - * Specify a user identifier which will be visible in the Firebase Crashlytics console. - * - * It is recommended for privacy purposes that this value be a value that's meaningless to a third-party - * observer; such as an arbitrary string that ties an end-user to a record in your system e.g. a database record id. - * - * #### Example - * - * ```js - * const auth = getAuth(); - * const crashlytics = getCrashlytics(); - * // Custom user id - * await setUserId(crashlytics, '123456789'); - * // Firebase auth uid - * await setUserId( - * crashlytics, - * auth.currentUser.uid - * ); - * ``` - */ -export declare function setUserId(crashlytics: FirebaseCrashlytics, userId: string): Promise; - -/** - * Sets a string value to be associated with the given attribute name which will be visible in the Firebase Crashlytics console. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * await setAttribute(crashlytics, 'role', 'admin'); - * ``` - */ -export declare function setAttribute( - crashlytics: FirebaseCrashlytics, - name: string, - value: string, -): Promise; - -/** - * Like `setAttribute` but for multiple attributes. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * await setAttributes(crashlytics, { - * role: 'admin', - * followers: '13', - * }); - * ``` - */ -export declare function setAttributes( - crashlytics: FirebaseCrashlytics, - attributes: { [key: string]: string }, -): Promise; - -/** - * Enable/disable Crashlytics reporting. - * - * Use this for opt-in first user data collection flows combined with `firebase.json` settings to disable auto collection. - * - * #### Example - * - * ```js - * const crashlytics = getCrashlytics(); - * // Disable crash reporting - * await setCrashlyticsCollectionEnabled(crashlytics, false); - * ``` - */ -export declare function setCrashlyticsCollectionEnabled( - crashlytics: FirebaseCrashlytics, - enabled: boolean, -): Promise; diff --git a/packages/crashlytics/lib/index.js b/packages/crashlytics/lib/namespaced.ts similarity index 68% rename from packages/crashlytics/lib/index.js rename to packages/crashlytics/lib/namespaced.ts index 73c2d668b4..9cf826334c 100644 --- a/packages/crashlytics/lib/index.js +++ b/packages/crashlytics/lib/namespaced.ts @@ -25,6 +25,7 @@ import { isOther, MODULAR_DEPRECATION_ARG, } from '@react-native-firebase/app/lib/common'; +import type { ModuleConfig } from '@react-native-firebase/app/lib/internal'; import { createModuleNamespace, FirebaseModule, @@ -36,28 +37,36 @@ import { setGlobalErrorHandler, setOnUnhandledPromiseRejectionHandler, } from './handlers'; -import version from './version'; +import { version } from './version'; +import type { Crashlytics, CrashlyticsStatics } from './types/crashlytics'; +import type { ReactNativeFirebase } from '@react-native-firebase/app'; -const statics = {}; +const statics: CrashlyticsStatics = {}; const namespace = 'crashlytics'; const nativeModuleName = 'RNFBCrashlyticsModule'; class FirebaseCrashlyticsModule extends FirebaseModule { - constructor(...args) { - super(...args); + _isCrashlyticsCollectionEnabled: boolean; + + constructor( + app: ReactNativeFirebase.FirebaseAppBase, + config: ModuleConfig, + customUrlOrRegion?: string | null, + ) { + super(app, config, customUrlOrRegion); setGlobalErrorHandler(this.native); setOnUnhandledPromiseRejectionHandler(this.native); this._isCrashlyticsCollectionEnabled = this.native.isCrashlyticsCollectionEnabled; } - get isCrashlyticsCollectionEnabled() { + get isCrashlyticsCollectionEnabled(): boolean { // Purposefully did not deprecate this as I think it should remain a property rather than a method. return this._isCrashlyticsCollectionEnabled; } - checkForUnsentReports() { + checkForUnsentReports(): Promise { if (this.isCrashlyticsCollectionEnabled) { throw new Error( "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) has been set to 'true', all reports are automatically sent.", @@ -66,23 +75,23 @@ class FirebaseCrashlyticsModule extends FirebaseModule { return this.native.checkForUnsentReports(); } - crash() { + crash(): void { this.native.crash(); } - async deleteUnsentReports() { + async deleteUnsentReports(): Promise { await this.native.deleteUnsentReports(); } - didCrashOnPreviousExecution() { + didCrashOnPreviousExecution(): Promise { return this.native.didCrashOnPreviousExecution(); } - log(message) { + log(message: string): void { this.native.log(`${message}`); } - setAttribute(name, value) { + setAttribute(name: string, value: string): Promise { if (!isString(name)) { throw new Error( 'firebase.crashlytics().setAttribute(*, _): The supplied property name must be a string.', @@ -98,7 +107,7 @@ class FirebaseCrashlyticsModule extends FirebaseModule { return this.native.setAttribute(name, value); } - setAttributes(object) { + setAttributes(object: { [key: string]: string }): Promise { if (!isObject(object)) { throw new Error( 'firebase.crashlytics().setAttributes(*): The supplied arg must be an object of key value strings.', @@ -108,7 +117,7 @@ class FirebaseCrashlyticsModule extends FirebaseModule { return this.native.setAttributes(object); } - setUserId(userId) { + setUserId(userId: string): Promise { if (!isString(userId)) { throw new Error( 'firebase.crashlytics().setUserId(*): The supplied userId must be a string value.', @@ -118,7 +127,7 @@ class FirebaseCrashlyticsModule extends FirebaseModule { return this.native.setUserId(userId); } - recordError(error, jsErrorName) { + recordError(error: Error, jsErrorName?: string): void { if (isError(error)) { StackTrace.fromError(error, { offline: true }).then(stackFrames => { this.native.recordError(createNativeErrorObj(error, stackFrames, false, jsErrorName)); @@ -130,13 +139,13 @@ class FirebaseCrashlyticsModule extends FirebaseModule { } } - sendUnsentReports() { + sendUnsentReports(): void { if (this.isCrashlyticsCollectionEnabled) { this.native.sendUnsentReports(); } } - setCrashlyticsCollectionEnabled(enabled) { + setCrashlyticsCollectionEnabled(enabled: boolean): Promise { if (!isBoolean(enabled)) { throw new Error( "firebase.crashlytics().setCrashlyticsCollectionEnabled(*) 'enabled' must be a boolean.", @@ -148,14 +157,10 @@ class FirebaseCrashlyticsModule extends FirebaseModule { } } -export * from './modular'; - // import { SDK_VERSION } from '@react-native-firebase/crashlytics'; export const SDK_VERSION = version; -// import crashlytics from '@react-native-firebase/crashlytics'; -// crashlytics().X(...); -export default createModuleNamespace({ +const crashlyticsNamespace = createModuleNamespace({ statics, version, namespace, @@ -166,12 +171,36 @@ export default createModuleNamespace({ ModuleClass: FirebaseCrashlyticsModule, }); +type CrashlyticsNamespace = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp< + Crashlytics, + CrashlyticsStatics +> & { + crashlytics: ReactNativeFirebase.FirebaseModuleWithStaticsAndApp; + firebase: ReactNativeFirebase.Module; + app(name?: string): ReactNativeFirebase.FirebaseApp; +}; + +// import crashlytics from '@react-native-firebase/crashlytics'; +// crashlytics().X(...); +export default crashlyticsNamespace as unknown as CrashlyticsNamespace; + // import crashlytics, { firebase } from '@react-native-firebase/crashlytics'; // crashlytics().X(...); // firebase.crashlytics().X(...); -export const firebase = getFirebaseRoot(); +export const firebase = + getFirebaseRoot() as unknown as ReactNativeFirebase.FirebaseNamespacedExport< + 'crashlytics', + Crashlytics, + CrashlyticsStatics, + false + >; + +// Register the interop module for non-native platforms. +// Note: This package doesn't have a web fallback module like functions does +// setReactNativeModule(nativeModuleName, fallBackModule); // This will throw with 'Default App Not initialized' if the default app is not configured. if (!isOther) { + // @ts-ignore - Extra arg used by deprecation proxy to detect namespaced calls firebase.crashlytics.call(null, getApp(), MODULAR_DEPRECATION_ARG); } diff --git a/packages/crashlytics/lib/types/crashlytics.ts b/packages/crashlytics/lib/types/crashlytics.ts new file mode 100644 index 0000000000..92e697e37e --- /dev/null +++ b/packages/crashlytics/lib/types/crashlytics.ts @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +import type { ReactNativeFirebase } from '@react-native-firebase/app'; + +// ============ Module Interface ============ + +/** + * Crashlytics module instance - returned from firebase.crashlytics() or firebase.app().crashlytics() + */ +export interface Crashlytics extends ReactNativeFirebase.FirebaseModule { + /** The FirebaseApp this module is associated with */ + app: ReactNativeFirebase.FirebaseApp; + + /** + * Whether Crashlytics reporting is enabled. + */ + readonly isCrashlyticsCollectionEnabled: boolean; + + /** + * Determines whether there are any unsent crash reports cached on the device. + * The callback only executes if automatic data collection is disabled. + * + * @throws Error if Crashlytics collection is enabled (reports are automatically sent) + */ + checkForUnsentReports(): Promise; + + /** + * Cause your app to crash for testing purposes. This is a native crash and will not contain a javascript stack trace. + * Note that crashes are intercepted by debuggers on iOS so no report will be seen under those conditions. + */ + crash(): void; + + /** + * Deletes any unsent reports on the device. This method only applies if automatic data collection is disabled. + */ + deleteUnsentReports(): Promise; + + /** + * Returns a boolean value indicating whether the app crashed during the previous execution. + */ + didCrashOnPreviousExecution(): Promise; + + /** + * Log a message that will appear in any subsequent Crash or Non-fatal error reports. + * + * @param message The message to log. + */ + log(message: string): void; + + /** + * Record a JavaScript Error. + * + * The JavaScript stack trace is converted into a mock native iOS or Android exception before submission. + * + * @param error Expects an instance of Error; e.g. classes that extend Error will also be supported. + * @param jsErrorName Optional string containing Javascript error name + */ + recordError(error: Error, jsErrorName?: string): void; + + /** + * Enqueues any unsent reports on the device to upload to Crashlytics. This method only applies if + * automatic data collection is disabled. + */ + sendUnsentReports(): void; + + /** + * Sets a string value to be associated with the given attribute name which will be visible in the Firebase Crashlytics console. + * + * @param name The name of the attribute to set. + * @param value A string value for the given attribute. + */ + setAttribute(name: string, value: string): Promise; + + /** + * Like `setAttribute` but for multiple attributes. + * + * @param attributes An object of key/value attribute name and values. + */ + setAttributes(attributes: { [key: string]: string }): Promise; + + /** + * Specify a user identifier which will be visible in the Firebase Crashlytics console. + * + * It is recommended for privacy purposes that this value be a value that's meaningless to a third-party + * observer; such as an arbitrary string that ties an end-user to a record in your system e.g. a database record id. + * + * @param userId An arbitrary string that ties an end-user to a record in your system e.g. a database record id. + */ + setUserId(userId: string): Promise; + + /** + * Enable/disable Crashlytics reporting. + * + * Use this for opt-in first user data collection flows combined with `firebase.json` settings to disable auto collection. + * + * @param enabled A boolean value representing whether to enable Crashlytics error collection. + */ + setCrashlyticsCollectionEnabled(enabled: boolean): Promise; +} + +// ============ Statics Interface ============ + +/** + * Static properties available on firebase.crashlytics + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface CrashlyticsStatics {} + +/** + * FirebaseApp type with crashlytics() method. + * @deprecated Import FirebaseApp from '@react-native-firebase/app' instead. + * The crashlytics() method is added via module augmentation. + */ +export type FirebaseApp = ReactNativeFirebase.FirebaseApp; + +// ============ Module Augmentation ============ + +/* eslint-disable @typescript-eslint/no-namespace */ +declare module '@react-native-firebase/app' { + namespace ReactNativeFirebase { + interface Module { + crashlytics: FirebaseModuleWithStaticsAndApp; + } + interface FirebaseApp { + crashlytics(): Crashlytics; + } + } +} +/* eslint-enable @typescript-eslint/no-namespace */ + +// ============ Backwards Compatibility Namespace - to be removed with namespaced exports ============ + +// Helper types to reference outer scope types within the namespace +// These are needed because TypeScript can't directly alias types with the same name +type _Crashlytics = Crashlytics; +type _CrashlyticsStatics = CrashlyticsStatics; + +/** + * @deprecated Use the exported types directly instead. + * FirebaseCrashlyticsTypes namespace is kept for backwards compatibility. + */ +/* eslint-disable @typescript-eslint/no-namespace */ +export namespace FirebaseCrashlyticsTypes { + // Short name aliases referencing top-level types + export type Module = Crashlytics; + export type Statics = CrashlyticsStatics; + + // Full name aliases that reference the exported types above via helper types + export type Crashlytics = _Crashlytics; + export type CrashlyticsStatics = _CrashlyticsStatics; +} +/* eslint-enable @typescript-eslint/no-namespace */ diff --git a/packages/crashlytics/package.json b/packages/crashlytics/package.json index 0f24e262b5..ca726bc000 100644 --- a/packages/crashlytics/package.json +++ b/packages/crashlytics/package.json @@ -3,14 +3,16 @@ "version": "23.7.0", "author": "Invertase (http://invertase.io)", "description": "React Native Firebase - Firebase Crashlytics is a lightweight, realtime crash reporter that helps you track, prioritize, and fix stability issues that erode your app quality. React Native Firebase provides automatic crash reporting for both native and JavaScript errors, including unhandled promise rejections.", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./dist/commonjs/index.js", + "module": "./dist/module/index.js", + "types": "./dist/typescript/commonjs/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js", + "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", "build:plugin": "rimraf plugin/build && tsc --build plugin", "lint:plugin": "eslint plugin/src/*", - "prepare": "yarn run build && yarn run build:plugin" + "compile": "bob build", + "prepare": "yarn run build && yarn run build:plugin && yarn compile" }, "repository": { "type": "git", @@ -36,7 +38,9 @@ "stacktrace-js": "^2.0.2" }, "devDependencies": { - "expo": "^54.0.27" + "expo": "^54.0.27", + "react-native-builder-bob": "^0.40.12", + "typescript": "^5.8.3" }, "peerDependenciesMeta": { "expo": { @@ -46,5 +50,50 @@ "publishConfig": { "access": "public", "provenance": true + }, + "exports": { + ".": { + "source": "./lib/index.ts", + "import": { + "types": "./dist/typescript/module/lib/index.d.ts", + "default": "./dist/module/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/index.d.ts", + "default": "./dist/commonjs/index.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "lib/modular", + "dist", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], + "react-native-builder-bob": { + "source": "lib", + "output": "dist", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "commonjs", + { + "esm": true + } + ], + [ + "typescript", + { + "tsc": "../../node_modules/.bin/tsc" + } + ] + ] } } diff --git a/packages/crashlytics/tsconfig.json b/packages/crashlytics/tsconfig.json new file mode 100644 index 0000000000..093ab89182 --- /dev/null +++ b/packages/crashlytics/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true, + "baseUrl": ".", + "rootDir": ".", + "paths": { + "@react-native-firebase/app/lib/common/*": ["../app/dist/typescript/commonjs/lib/common/*"], + "@react-native-firebase/app/lib/common": ["../app/dist/typescript/commonjs/lib/common"], + "@react-native-firebase/app/lib/internal/web/*": [ + "../app/dist/typescript/commonjs/lib/internal/web/*" + ], + "@react-native-firebase/app/lib/internal/*": [ + "../app/dist/typescript/commonjs/lib/internal/*" + ], + "@react-native-firebase/app/lib/internal": ["../app/dist/typescript/commonjs/lib/internal"], + "@react-native-firebase/app": ["../app/dist/typescript/commonjs/lib"] + } + }, + "include": ["lib/**/*", "../app/lib/internal/global.d.ts"], + "exclude": ["node_modules", "dist", "__tests__", "**/*.test.ts"] +} diff --git a/packages/crashlytics/type-test.ts b/packages/crashlytics/type-test.ts index aabac49457..bd645bebea 100644 --- a/packages/crashlytics/type-test.ts +++ b/packages/crashlytics/type-test.ts @@ -1,5 +1,9 @@ import crashlytics, { firebase, + FirebaseCrashlyticsTypes, + // Types + type Crashlytics, + // Modular API getCrashlytics, checkForUnsentReports, deleteUnsentReports, @@ -33,10 +37,12 @@ console.log(crashlytics.firebase.SDK_VERSION); // checks root exists console.log(firebase.SDK_VERSION); -// checks Module instance APIs -const crashlyticsInstance = firebase.crashlytics(); +// test type usage +const crashlyticsInstance: Crashlytics = firebase.crashlytics(); +console.log(crashlyticsInstance.app.name); console.log(crashlyticsInstance.isCrashlyticsCollectionEnabled); +// checks Module instance APIs crashlyticsInstance.checkForUnsentReports().then((hasUnsent: boolean) => { console.log(hasUnsent); }); @@ -74,7 +80,33 @@ crashlyticsInstance.setCrashlyticsCollectionEnabled(true).then(() => { console.log('Collection enabled'); }); -// checks modular API functions +// checks all methods exist on firebase.crashlytics() +console.log(firebase.crashlytics().checkForUnsentReports); +console.log(firebase.crashlytics().deleteUnsentReports); +console.log(firebase.crashlytics().didCrashOnPreviousExecution); +console.log(firebase.crashlytics().crash); +console.log(firebase.crashlytics().log); +console.log(firebase.crashlytics().recordError); +console.log(firebase.crashlytics().sendUnsentReports); +console.log(firebase.crashlytics().setUserId); +console.log(firebase.crashlytics().setAttribute); +console.log(firebase.crashlytics().setAttributes); +console.log(firebase.crashlytics().setCrashlyticsCollectionEnabled); + +// checks all methods exist on default export +console.log(crashlytics().checkForUnsentReports); +console.log(crashlytics().deleteUnsentReports); +console.log(crashlytics().didCrashOnPreviousExecution); +console.log(crashlytics().crash); +console.log(crashlytics().log); +console.log(crashlytics().recordError); +console.log(crashlytics().sendUnsentReports); +console.log(crashlytics().setUserId); +console.log(crashlytics().setAttribute); +console.log(crashlytics().setAttributes); +console.log(crashlytics().setCrashlyticsCollectionEnabled); + +// test modular API functions const crashlyticsModular = getCrashlytics(); console.log(crashlyticsModular.app.name); @@ -114,3 +146,11 @@ setAttributes(crashlyticsInstance, { key1: 'value1', key2: 'value2' }).then(() = setCrashlyticsCollectionEnabled(crashlyticsInstance, true).then(() => { console.log('Collection enabled'); }); + +// test FirebaseCrashlyticsTypes namespace +const namespaceInstance: FirebaseCrashlyticsTypes.Module = firebase.crashlytics(); +console.log(namespaceInstance.app.name); +const namespaceStatics: FirebaseCrashlyticsTypes.Statics = firebase.crashlytics; +console.log(namespaceStatics); +const namespaceCrashlytics: FirebaseCrashlyticsTypes.Crashlytics = firebase.crashlytics(); +console.log(namespaceCrashlytics.app.name); diff --git a/yarn.lock b/yarn.lock index a46ffdba47..61b92a2763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5127,7 +5127,9 @@ __metadata: resolution: "@react-native-firebase/crashlytics@workspace:packages/crashlytics" dependencies: expo: "npm:^54.0.27" + react-native-builder-bob: "npm:^0.40.12" stacktrace-js: "npm:^2.0.2" + typescript: "npm:^5.8.3" peerDependencies: "@react-native-firebase/app": 23.7.0 expo: ">=47.0.0"