Skip to content

Commit

Permalink
Firebase Reads Logger
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamFilipek92 committed Jun 2, 2020
1 parent 2cff86d commit 7a57c8d
Show file tree
Hide file tree
Showing 11 changed files with 730 additions and 185 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-admin-firebase-test",
"description": "A firebase data provider for the React Admin framework",
"version": "3.2.13",
"version": "3.2.19",
"peerDependencies": {
"firebase": "^7.9.x",
"react": "^16.x",
Expand Down
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { DataProvider } from "./providers/DataProvider";
import { AuthProvider } from "./providers/AuthProvider";
import { RAFirebaseOptions } from "./providers/RAFirebaseOptions";
import ReadsLogger, { getFirebaseReadsLogger } from "./misc/reads-logger";

export {
DataProvider as FirebaseDataProvider,
AuthProvider as FirebaseAuthProvider,
RAFirebaseOptions as RAFirebaseOptions
RAFirebaseOptions as RAFirebaseOptions,
ReadsLogger as FirebaseReadsLogger,
getFirebaseReadsLogger
};
178 changes: 178 additions & 0 deletions src/misc/reads-logger/ReadsLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { RAFirebaseOptions } from "../../providers/RAFirebaseOptions";
import { RAFirebaseReadsLoggerError } from "./types";
import ReadsLoggerLocalStorageUtils from "./ReadsLoggerLocalStorageUtils";
import { log, logError } from "../logger";
import { BehaviorSubject, Observable } from "rxjs";

type ResetFn = () => void;
type IncrementFn = (newReads: number) => void;

interface CounterStreams<TStreamOrSubject extends Observable<number> = Observable<number>> {
pageReads: TStreamOrSubject;
lastPageReads: TStreamOrSubject;
customReads: TStreamOrSubject;
lastCustomReads: TStreamOrSubject;
sessionReads: TStreamOrSubject;
lastSessionReads: TStreamOrSubject;
}

export default class ReadsLogger {
/*
* CURRENT PAGE FIREBASE READS COUNTER INTERFACE
*
* pageReads should be used to track firebase reads for displayed page.
* It means that resetPageReads() should be called on every navigation
* in react-admin application
*
* lastPageReads will contain reads for previously displayed page
*/
public get pageReads(): number {
return this._pageReads;
}
public get lastPageReads(): number {
return this._lastPageReads;
}
public resetPageReads: ResetFn = () => {
this._lastPageReads = this.pageReads;
this._pageReads = 0;
this.subjects.pageReads.next(this.pageReads);
this.subjects.lastPageReads.next(this.lastPageReads);
};
public incrementPageReads: IncrementFn = newReads => {
this._pageReads += newReads;
this.subjects.pageReads.next(this.pageReads);
};
private _lastPageReads: number = 0;
private _pageReads: number = 0;
/*
* CUSTOM FIREBASE READS COUNTER INTERFACE
*
* customReads should be used to track user controlled
* period of firebase reads
* It means that resetCustomReads() should be called after
* click on Reset Custom Counter button
*
* lastCustomReads will contain reads for previous custom counter
*/
public get customReads(): number {
return this.storage.getReads('customReads');
}
public get lastCustomReads(): number {
return this.storage.getReads('lastCustomReads');
}
public resetCustomReads: ResetFn = () => {
this.storage.resetReads('customReads');
this.subjects.customReads.next(this.customReads);
this.subjects.lastCustomReads.next(this.lastCustomReads);
};
public incrementCustomReads: IncrementFn = newReads => {
this.storage.incrementReads(newReads, 'customReads');
this.subjects.customReads.next(this.customReads);
};
/*
* USER SESSION FIREBASE READS COUNTER INTERFACE
*
* sessionReads should be used to track all firebase reads
* in user session.
* It means that resetSessionReads() should be called on every logout from
* react-admin application
*
* lastSessionReads will contain reads for previous user session
*/
public get sessionReads(): number {
return this.storage.getReads('sessionReads');
}
public get lastSessionReads(): number {
return this.storage.getReads('lastSessionReads');
}
public resetSessionReads: ResetFn = () => {
this.storage.resetReads('sessionReads');
this.subjects.sessionReads.next(this.sessionReads);
this.subjects.lastSessionReads.next(this.lastSessionReads);
};
public incrementSessionReads: IncrementFn = newReads => {
this.storage.incrementReads(newReads, 'sessionReads');
this.subjects.sessionReads.next(this.sessionReads);
};
/*
* INCREMENT ALL COUNTERS
*/
public incrementAll: IncrementFn = newReads => {
this.incrementPageReads(newReads);
this.incrementCustomReads(newReads);
this.incrementSessionReads(newReads);
};
/*
* STREAMS
*/
private readonly subjects: CounterStreams<BehaviorSubject<number>>;
public get streams(): CounterStreams {
return {
pageReads: this.subjects.pageReads.asObservable(),
lastPageReads: this.subjects.lastPageReads.asObservable(),
customReads: this.subjects.customReads.asObservable(),
lastCustomReads: this.subjects.lastCustomReads.asObservable(),
sessionReads: this.subjects.sessionReads.asObservable(),
lastSessionReads: this.subjects.lastSessionReads.asObservable()
}
}

/*
* Private constructor - reads logger will be singleton
*/
private constructor(
private storage: ReadsLoggerLocalStorageUtils
) {
this.subjects = this.initCounterSubjects();
}

private initCounterSubjects = () => ({
pageReads: new BehaviorSubject(this.pageReads),
lastPageReads: new BehaviorSubject(this.lastPageReads),
customReads: new BehaviorSubject(this.customReads),
lastCustomReads: new BehaviorSubject(this.lastCustomReads),
sessionReads: new BehaviorSubject(this.sessionReads),
lastSessionReads: new BehaviorSubject(this.lastSessionReads)
});

/*
* Statics
*
* private __instance - ReadsLogger singleton instance
*
* ReadsLogger.initLogger(options) - initializing ReadsLogger singleton
* instance
*
* ReadsLogger.getLogger() - get ReadsLogger singleton instance
*/
private static __instance: ReadsLogger = null;

public static initLogger(options: RAFirebaseOptions) {
const loggingEnabled = options.lazyLoading &&
options.lazyLoading.firebaseReadsLogging &&
options.lazyLoading.firebaseReadsLogging.enabled;

if (!loggingEnabled) {
throw new RAFirebaseReadsLoggerError('Logging not enabled');
}

if (!ReadsLogger.__instance) {
const storageUtils = new ReadsLoggerLocalStorageUtils(options);
storageUtils.init()
.then(() => {
ReadsLogger.__instance = new ReadsLogger(storageUtils);
log('Firebase Reads Logger Initialized!');
})
.catch(error => {
logError(error.message);
})
}
}

public static getLogger(): ReadsLogger {
if (!ReadsLogger.__instance) {
throw new RAFirebaseReadsLoggerError('Logger Not Initialized!');
}
return ReadsLogger.__instance;
}
}
140 changes: 140 additions & 0 deletions src/misc/reads-logger/ReadsLoggerLocalStorageUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { RAFirebaseReadsLoggerError, RAFirebaseReadsLoggerStorageKeys } from "./types";
import { RAFirebaseOptions, RAFirebaseReadsLoggingOptions } from "../../providers/RAFirebaseOptions";
import firebase from "firebase/app";
import "firebase/auth";

type ReadsKey = 'customReads' | 'sessionReads';
type GetReadsFn = (readsKey: keyof RAFirebaseReadsLoggerStorageKeys) => number;
type ResetReadsFn = (readsKey: ReadsKey) => void;
type IncrementReadsFn = (newReads: number, readsKey: ReadsKey) => void;

export default class ReadsLoggerLocalStorageUtils {
/*
* PRIVATE FIELDS
*/
private storageKeys: RAFirebaseReadsLoggerStorageKeys = null;
private get hasKeys(): boolean {
return !!this.storageKeys;
}
private readonly loggerOptions: RAFirebaseReadsLoggingOptions;
private readonly DEFAULT_PREFIX: string = 'ra-firebase-reads';
private isInitialized: boolean = false;
/*
* PUBLIC LOCAL STORAGE UTILS INTERFACE
*/
public authStateUnsubscribe: firebase.Unsubscribe;

public get keys(): RAFirebaseReadsLoggerStorageKeys {
return this.storageKeys;
}

public getReads: GetReadsFn = readsKey => {
if (!this.hasKeys) {
return 0;
}
const key = this.keys[readsKey];
const readsFromStorage = localStorage.getItem(key);
if (readsFromStorage === null) {
this.setReadsInStorage(readsKey, 0);
return 0;
}

return parseInt(readsFromStorage, 10);
};

public resetReads: ResetReadsFn = readsKey => {
this.checkErrors();
const lastReadsKey = readsKey === 'customReads' ?
'lastCustomReads' : 'lastSessionReads';
const newLastReads = this.getReads(readsKey);
this.setReadsInStorage(lastReadsKey, newLastReads);
this.setReadsInStorage(readsKey, 0);
};

public incrementReads: IncrementReadsFn = (newReads, readsKey) => {
this.checkErrors();
const oldReads: number = this.getReads(readsKey);
const incrementedReads = oldReads + newReads;
this.setReadsInStorage(readsKey, incrementedReads);
};

/*
* Init
*
* Should be called before any action in
*/
public async init() {
if (this.isInitialized) {
throw new RAFirebaseReadsLoggerError('Storage utils already initialized.');
}
this.isInitialized = await this.startUserSubscription();
return this.isInitialized;
}
/*
* Class will be only imported in ReadsLogger file
* and constructor will be only called in ReadsLogger.init()
*/
constructor(
private readonly options: RAFirebaseOptions
) {
this.loggerOptions = this.options.lazyLoading.firebaseReadsLogging;
}
/*
* Initial subscription for user id to save counters for user
*/
private async startUserSubscription(): Promise<boolean> {
return new Promise((resolve, reject) => {
const firebaseAuth = this.options.app.auth() || firebase.app().auth();
this.authStateUnsubscribe = firebaseAuth.onAuthStateChanged(
userSnapshot => {
if (userSnapshot.uid) {
this.storageKeys = this.getLocalStorageKeys(userSnapshot.uid);
resolve(true);
}
},
error => reject(error)
);
});
}
/*
* INTERNAL UTILS
*/
private setReadsInStorage(
key: keyof RAFirebaseReadsLoggerStorageKeys,
reads: number
): void {
localStorage.setItem(this.keys[key], String(reads));
}

private getLocalStorageKeys(
userId: string
): RAFirebaseReadsLoggerStorageKeys {
const customPrefix = this.loggerOptions &&
this.loggerOptions.localStoragePrefix;
const prefix = customPrefix || this.DEFAULT_PREFIX;

return {
customReads: `${prefix}-${userId}-custom-reads`,
lastCustomReads: `${prefix}-${userId}-last-custom-reads`,
sessionReads: `${prefix}-${userId}-session-reads`,
lastSessionReads: `${prefix}-${userId}-last-session-reads`
};
}

private checkErrors() {
this.checkInitError();
this.checkKeysError();
}

private checkKeysError() {
if (!this.hasKeys) {
throw new RAFirebaseReadsLoggerError('Cannot read local storage keys.');
}
}

private checkInitError() {
if (!this.isInitialized) {
throw new RAFirebaseReadsLoggerError('Storage utils not initialized.');
}
}
}
10 changes: 10 additions & 0 deletions src/misc/reads-logger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ReadsLogger from "./ReadsLogger";
import { RAFirebaseOptions } from "../../providers/RAFirebaseOptions";

export const initFirebaseReadsLogger = (options: RAFirebaseOptions) => {
ReadsLogger.initLogger(options);
};

export const getFirebaseReadsLogger = () => ReadsLogger.getLogger();

export default ReadsLogger;
12 changes: 12 additions & 0 deletions src/misc/reads-logger/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface RAFirebaseReadsLoggerStorageKeys {
customReads: string;
lastCustomReads: string;
sessionReads: string;
lastSessionReads: string;
}

export class RAFirebaseReadsLoggerError extends ReferenceError {
constructor(message: string) {
super(`RAFirebaseReadsLoggerError: ${message}`);
}
}
7 changes: 6 additions & 1 deletion src/providers/DataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import {
messageTypes,
retrieveStatusCode,
} from "../misc";
import { RAFirebaseOptions } from "./RAFirebaseOptions";
import { isReadsLoggingEnabled, RAFirebaseOptions } from "./RAFirebaseOptions";
import { FirebaseClient } from "./database/FirebaseClient";
import { FirebaseWrapper } from "./database/firebase/FirebaseWrapper";
import { initFirebaseReadsLogger } from '../misc/reads-logger';

export let fb: FirebaseClient;

Expand All @@ -37,6 +38,10 @@ export function DataProvider(
});
const fireWrapper = new FirebaseWrapper();
fireWrapper.init(firebaseConfig, optionsInput);
if (isReadsLoggingEnabled(options)) {
initFirebaseReadsLogger(options);
}

fb = new FirebaseClient(fireWrapper, options);
async function providerApi(
type: string,
Expand Down
Loading

0 comments on commit 7a57c8d

Please sign in to comment.