Skip to content

Client logging API: methods for users to access the SDK's log messages #2434

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Mar 13, 2020
Merged
1 change: 1 addition & 0 deletions packages/analytics/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"dependencies": {
"@firebase/analytics-types": "0.2.8",
"@firebase/installations": "0.4.5",
"@firebase/logger": "0.1.37",
"@firebase/util": "0.2.42",
"@firebase/component": "0.1.7",
"tslib": "1.11.1"
Expand Down
5 changes: 3 additions & 2 deletions packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
GTAG_URL
} from './constants';
import { FirebaseInstallations } from '@firebase/installations-types';
import { logger } from './logger';

/**
* Initialize the analytics instance in gtag.js by calling config command with fid.
Expand Down Expand Up @@ -146,7 +147,7 @@ function wrapGtag(
gtagParams || {}
)
)
.catch(e => console.error(e));
.catch(e => logger.error(e));
} else if (command === GtagCommand.CONFIG) {
const initializationPromiseToWait =
initializedIdPromisesMap[idOrNameOrParams as string] ||
Expand All @@ -155,7 +156,7 @@ function wrapGtag(
.then(() => {
gtagCore(GtagCommand.CONFIG, idOrNameOrParams as string, gtagParams);
})
.catch(e => console.error(e));
.catch(e => logger.error(e));
} else {
// SET command.
// Splitting calls for CONFIG and SET to make it clear which signature
Expand Down
20 changes: 20 additions & 0 deletions packages/analytics/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @license
* Copyright 2019 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file 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 { Logger } from '@firebase/logger';

export const logger = new Logger('@firebase/analytics');
7 changes: 7 additions & 0 deletions packages/app-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { LogCallback, LogLevelString, LogOptions } from '@firebase/logger';

export type FirebaseOptions = {
apiKey?: string;
Expand Down Expand Up @@ -105,6 +106,12 @@ export interface FirebaseNamespace {
*/
registerVersion(library: string, version: string, variant?: string): void;

// Sets log level for all Firebase components.
setLogLevel(logLevel: LogLevelString): void;

// Sets log handler for all Firebase components.
onLog(logCallback: LogCallback, options?: LogOptions): void;

// The current SDK version.
SDK_VERSION: string;
}
Expand Down
6 changes: 4 additions & 2 deletions packages/app/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export const enum AppError {
BAD_APP_NAME = 'bad-app-name',
DUPLICATE_APP = 'duplicate-app',
APP_DELETED = 'app-deleted',
INVALID_APP_ARGUMENT = 'invalid-app-argument'
INVALID_APP_ARGUMENT = 'invalid-app-argument',
INVALID_LOG_ARGUMENT = 'invalid-log-argument'
}

const ERRORS: ErrorMap<AppError> = {
Expand All @@ -34,7 +35,8 @@ const ERRORS: ErrorMap<AppError> = {
[AppError.APP_DELETED]: "Firebase App named '{$appName}' already deleted",
[AppError.INVALID_APP_ARGUMENT]:
'firebase.{$appName}() takes either no argument or a ' +
'Firebase App instance.'
'Firebase App instance.',
[AppError.INVALID_LOG_ARGUMENT]: 'First argument to `onLog` must be null or a function.'
};

type ErrorParams = { [key in AppError]: { appName: string } };
Expand Down
17 changes: 17 additions & 0 deletions packages/app/src/firebaseNamespaceCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ import { FirebaseAppLiteImpl } from './lite/firebaseAppLite';
import { DEFAULT_ENTRY_NAME, PLATFORM_LOG_STRING } from './constants';
import { version } from '../../firebase/package.json';
import { logger } from './logger';
import {
setUserLogHandler,
setLogLevel,
LogCallback,
LogOptions
} from '@firebase/logger';
import { Component, ComponentType, Name } from '@firebase/component';

/**
Expand All @@ -60,6 +66,8 @@ export function createFirebaseNamespaceCore(
// @ts-ignore
app,
registerVersion,
setLogLevel,
onLog,
// @ts-ignore
apps: null,
SDK_VERSION: version,
Expand Down Expand Up @@ -277,6 +285,15 @@ export function createFirebaseNamespaceCore(
);
}

function onLog(logCallback: LogCallback | null, options?: LogOptions): void {
if (logCallback !== null && typeof logCallback !== 'function') {
throw ERROR_FACTORY.create(AppError.INVALID_LOG_ARGUMENT, {
appName: name
});
}
setUserLogHandler(logCallback, options);
}

// Map the requested service to a registered service name
// (used to map auth to serverAuth service when needed).
function useAsService(app: FirebaseApp, name: string): string | null {
Expand Down
105 changes: 105 additions & 0 deletions packages/app/test/clientLogger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @license
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file 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 { FirebaseNamespace, VersionService } from '@firebase/app-types';
import { _FirebaseApp, _FirebaseNamespace } from '@firebase/app-types/private';
import { createFirebaseNamespace } from '../src/firebaseNamespace';
import { expect } from 'chai';
import { spy as Spy } from 'sinon';
import './setup';
import { Logger } from '@firebase/logger';
import { registerCoreComponents } from '../src/registerCoreComponents';
import {
Component,
ComponentType
} from '@firebase/component';

declare module '@firebase/component' {
interface NameServiceMapping {
'vs1': VersionService;
'vs2': VersionService;
'test-shell': Promise<void>;
}
}

describe('User Log Methods', () => {
describe('Integration Tests', () => {
let firebase: FirebaseNamespace;
let result: any = null;
const warnSpy = Spy(console, 'warn');
const infoSpy = Spy(console, 'info');
const logSpy = Spy(console, 'log');

beforeEach(() => {
firebase = createFirebaseNamespace();
});

it(`respects log level set through firebase.setLogLevel()`, () => {
firebase.initializeApp({});
registerCoreComponents(firebase);
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
new Component(
'test-shell',
async () => {
const logger = new Logger('@firebase/logger-test');
logger.warn('hello');
expect(warnSpy.called).to.be.true;
(firebase as _FirebaseNamespace).setLogLevel('warn');
logger.info('hi');
expect(infoSpy.called).to.be.false;
logger.log('hi');
expect(logSpy.called).to.be.false;
logSpy.resetHistory();
infoSpy.resetHistory();
(firebase as _FirebaseNamespace).setLogLevel('debug');
logger.info('hi');
expect(infoSpy.called).to.be.true;
logger.log('hi');
expect(logSpy.called).to.be.true;
},
ComponentType.PUBLIC
)
);
return (firebase as any)['test-shell']();
});

it(`correctly triggers callback given to firebase.onLog()`, () => {
// Note: default log level is INFO.
firebase.initializeApp({});
registerCoreComponents(firebase);
(firebase as _FirebaseNamespace).INTERNAL.registerComponent(
new Component(
'test-shell',
async () => {
const logger = new Logger('@firebase/logger-test');
(firebase as _FirebaseNamespace).onLog((logData) => {
result = logData;
});
logger.info('hi');
expect(result.level).to.equal('info');
expect(result.message).to.equal('hi');
expect(result.args).to.deep.equal(['hi']);
expect(result.type).to.equal('@firebase/logger-test');
expect(infoSpy.called).to.be.true;
},
ComponentType.PUBLIC
)
);
return (firebase as any)['test-shell']();
});
});
});
52 changes: 52 additions & 0 deletions packages/firebase/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ declare namespace firebase {
complete: CompleteFn;
}

/**
* The JS SDK supports 5 log levels and also allows a user the ability to
* silence the logs altogether.
*
* The order is as follows:
* silent < debug < verbose < info < warn < error
*/
type LogLevel = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent';

/**
* The current SDK version.
*/
Expand All @@ -95,6 +104,49 @@ declare namespace firebase {
variant?: string
): void;

/**
* Sets log level for all Firebase packages.
*
* All of the log types above the current log level are captured (i.e. if
* you set the log level to `info`, errors are logged, but `debug` and
* `verbose` logs are not).
*/
function setLogLevel(logLevel: LogLevel): void;

/**
* Sets log handler for all Firebase packages.
* @param logCallback An optional custom log handler that executes user code whenever
* the Firebase SDK makes a logging call.
*/
function onLog(
logCallback: (callbackParams: {
/**
* Level of event logged.
*/
level: LogLevel;
/**
* Any text from logged arguments joined into one string.
*/
message: string;
/**
* The raw arguments passed to the log call.
*/
args: unknown[];
/**
* A string indicating the name of the package that made the log call,
* such as `@firebase/firestore`.
*/
type: string;
}) => void,
options?: {
/**
* Threshhold log level. Only logs at or above this level trigger the `logCallback`
* passed to `onLog`.
*/
level: LogLevel;
}
): void;

/**
* @hidden
*/
Expand Down
19 changes: 10 additions & 9 deletions packages/logger/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
* limitations under the License.
*/

import { instances, LogLevel } from './src/logger';

export function setLogLevel(level: LogLevel): void {
instances.forEach(inst => {
inst.logLevel = level;
});
}

export { Logger, LogLevel, LogHandler } from './src/logger';
export {
setLogLevel,
Logger,
LogLevel,
LogHandler,
setUserLogHandler,
LogCallback,
LogLevelString,
LogOptions
} from './src/logger';
4 changes: 1 addition & 3 deletions packages/logger/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,14 @@
* limitations under the License.
*/

const karma = require('karma');
const path = require('path');
const karmaBase = require('../../config/karma.base');

const files = [`test/**/*`];

module.exports = function(config) {
const karmaConfig = Object.assign({}, karmaBase, {
// files to load into karma
files: files,
files,
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha']
Expand Down
Loading