From 655798e49dac41a6e96d1f14388bd76b35c47f0f Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 25 Feb 2022 15:14:36 -0800 Subject: [PATCH 1/3] Auth heartbeat implementation --- packages/auth-compat/src/auth.test.ts | 11 ++- .../auth-compat/src/popup_redirect.test.ts | 3 +- packages/auth-compat/test/helpers/helpers.ts | 8 ++ packages/auth/demo/src/index.js | 78 +++++++++---------- packages/auth/src/api/index.ts | 3 +- packages/auth/src/core/auth/auth_impl.test.ts | 29 ++++++- packages/auth/src/core/auth/auth_impl.ts | 11 +++ packages/auth/src/core/auth/register.ts | 9 ++- .../auth/src/platform_browser/auth.test.ts | 8 +- packages/auth/test/helpers/mock_auth.ts | 13 +++- 10 files changed, 117 insertions(+), 56 deletions(-) diff --git a/packages/auth-compat/src/auth.test.ts b/packages/auth-compat/src/auth.test.ts index 0299170827f..af49682afed 100644 --- a/packages/auth-compat/src/auth.test.ts +++ b/packages/auth-compat/src/auth.test.ts @@ -24,6 +24,7 @@ import sinonChai from 'sinon-chai'; import { Auth } from './auth'; import { CompatPopupRedirectResolver } from './popup_redirect'; import * as platform from './platform'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers'; use(sinonChai); @@ -41,9 +42,13 @@ describe('auth compat', () => { beforeEach(() => { app = { options: { apiKey: 'api-key' } } as FirebaseApp; - underlyingAuth = new exp.AuthImpl(app, { - apiKey: 'api-key' - } as exp.ConfigInternal); + underlyingAuth = new exp.AuthImpl( + app, + FAKE_HEARTBEAT_CONTROLLER_PROVIDER, + { + apiKey: 'api-key' + } as exp.ConfigInternal + ); sinon.stub(underlyingAuth, '_initializeWithPersistence'); providerStub = sinon.createStubInstance(Provider); diff --git a/packages/auth-compat/src/popup_redirect.test.ts b/packages/auth-compat/src/popup_redirect.test.ts index be01b8f622e..80bc382e4fa 100644 --- a/packages/auth-compat/src/popup_redirect.test.ts +++ b/packages/auth-compat/src/popup_redirect.test.ts @@ -22,6 +22,7 @@ import * as exp from '@firebase/auth/internal'; import * as platform from './platform'; import { CompatPopupRedirectResolver } from './popup_redirect'; import { FirebaseApp } from '@firebase/app-compat'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER } from '../test/helpers/helpers'; use(sinonChai); @@ -41,7 +42,7 @@ describe('popup_redirect/CompatPopupRedirectResolver', () => { beforeEach(() => { compatResolver = new CompatPopupRedirectResolver(); const app = { options: { apiKey: 'api-key' } } as FirebaseApp; - auth = new exp.AuthImpl(app, { + auth = new exp.AuthImpl(app, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: 'api-key' } as exp.ConfigInternal); }); diff --git a/packages/auth-compat/test/helpers/helpers.ts b/packages/auth-compat/test/helpers/helpers.ts index 005f8efa61a..aa5069461c6 100644 --- a/packages/auth-compat/test/helpers/helpers.ts +++ b/packages/auth-compat/test/helpers/helpers.ts @@ -17,6 +17,7 @@ import * as sinon from 'sinon'; import firebase from '@firebase/app-compat'; +import { Provider } from '@firebase/component'; import '../..'; import * as exp from '@firebase/auth/internal'; @@ -26,6 +27,13 @@ import { } from '../../../auth/test/helpers/integration/settings'; import { resetEmulator } from '../../../auth/test/helpers/integration/emulator_rest_helpers'; +// Heartbeat is fully tested in core auth impl +export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER = { + getImmediate(): undefined { + return undefined; + } +} as unknown as Provider<'heartbeat'>; + export function initializeTestInstance(): void { firebase.initializeApp(getAppConfig()); const stub = stubConsoleToSilenceEmulatorWarnings(); diff --git a/packages/auth/demo/src/index.js b/packages/auth/demo/src/index.js index 884110d3408..0e3bf6ac93f 100644 --- a/packages/auth/demo/src/index.js +++ b/packages/auth/demo/src/index.js @@ -1750,45 +1750,45 @@ function initApp() { } // Install servicerWorker if supported. - if ('serviceWorker' in navigator) { - navigator.serviceWorker - .register('/service-worker.js') - .then(reg => { - // Registration worked. - console.log('Registration succeeded. Scope is ' + reg.scope); - }) - .catch(error => { - // Registration failed. - console.log('Registration failed with ' + error.message); - }); - } - - if (window.Worker) { - webWorker = new Worker('/web-worker.js'); - /** - * Handles the incoming message from the web worker. - * @param {!Object} e The message event received. - */ - webWorker.onmessage = function (e) { - console.log('User data passed through web worker: ', e.data); - switch (e.data.type) { - case 'GET_USER_INFO': - alertSuccess( - 'User data passed through web worker: ' + JSON.stringify(e.data) - ); - break; - case 'RUN_TESTS': - if (e.data.status === 'success') { - alertSuccess('Web worker tests ran successfully!'); - } else { - alertError('Error: ' + JSON.stringify(e.data.error)); - } - break; - default: - return; - } - }; - } + // if ('serviceWorker' in navigator) { + // navigator.serviceWorker + // .register('/service-worker.js') + // .then(reg => { + // // Registration worked. + // console.log('Registration succeeded. Scope is ' + reg.scope); + // }) + // .catch(error => { + // // Registration failed. + // console.log('Registration failed with ' + error.message); + // }); + // } + + // if (window.Worker) { + // webWorker = new Worker('/web-worker.js'); + // /** + // * Handles the incoming message from the web worker. + // * @param {!Object} e The message event received. + // */ + // webWorker.onmessage = function (e) { + // console.log('User data passed through web worker: ', e.data); + // switch (e.data.type) { + // case 'GET_USER_INFO': + // alertSuccess( + // 'User data passed through web worker: ' + JSON.stringify(e.data) + // ); + // break; + // case 'RUN_TESTS': + // if (e.data.status === 'success') { + // alertSuccess('Web worker tests ran successfully!'); + // } else { + // alertError('Error: ' + JSON.stringify(e.data.error)); + // } + // break; + // default: + // return; + // } + // }; + // } /** * Asks the web worker, if supported in current browser, to return the user info diff --git a/packages/auth/src/api/index.ts b/packages/auth/src/api/index.ts index f727d8d2f66..a3375a2829c 100644 --- a/packages/auth/src/api/index.ts +++ b/packages/auth/src/api/index.ts @@ -37,7 +37,8 @@ export const enum HttpHeader { CONTENT_TYPE = 'Content-Type', X_FIREBASE_LOCALE = 'X-Firebase-Locale', X_CLIENT_VERSION = 'X-Client-Version', - X_FIREBASE_GMPID = 'X-Firebase-gmpid' + X_FIREBASE_GMPID = 'X-Firebase-gmpid', + X_FIREBASE_CLIENT = 'X-Firebase-Client', } export const enum Endpoint { diff --git a/packages/auth/src/core/auth/auth_impl.test.ts b/packages/auth/src/core/auth/auth_impl.test.ts index 4e226973a0c..bfc8ca75223 100644 --- a/packages/auth/src/core/auth/auth_impl.test.ts +++ b/packages/auth/src/core/auth/auth_impl.test.ts @@ -23,7 +23,7 @@ import sinonChai from 'sinon-chai'; import { FirebaseApp } from '@firebase/app'; import { FirebaseError } from '@firebase/util'; -import { testAuth, testUser } from '../../../test/helpers/mock_auth'; +import { FAKE_HEARTBEAT_CONTROLLER, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, testAuth, testUser } from '../../../test/helpers/mock_auth'; import { AuthInternal } from '../../model/auth'; import { UserInternal } from '../../model/user'; import { PersistenceInternal } from '../persistence'; @@ -53,7 +53,7 @@ describe('core/auth/auth_impl', () => { beforeEach(async () => { persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -431,7 +431,7 @@ describe('core/auth/auth_impl', () => { }); it('prevents initialization from completing', async () => { - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -474,6 +474,29 @@ describe('core/auth/auth_impl', () => { 'X-Client-Version': 'v', 'X-Firebase-gmpid': 'app-id', }); + delete auth.app.options.appId; + }); + + it('adds the heartbeat if available', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER, 'getHeartbeatsHeader').returns(Promise.resolve('heartbeat')); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + 'X-Firebase-Client': 'heartbeat', + }); + }); + + it('does not add heartbeat if none returned', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER, 'getHeartbeatsHeader').returns(Promise.resolve('')); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + }); + }); + + it('does not add heartbeat if controller unavailable', async () => { + sinon.stub(FAKE_HEARTBEAT_CONTROLLER_PROVIDER, 'getImmediate').returns(undefined as any); + expect(await auth._getAdditionalHeaders()).to.eql({ + 'X-Client-Version': 'v', + }); }); }); }); diff --git a/packages/auth/src/core/auth/auth_impl.ts b/packages/auth/src/core/auth/auth_impl.ts index b71e4e24bb4..d20c1596874 100644 --- a/packages/auth/src/core/auth/auth_impl.ts +++ b/packages/auth/src/core/auth/auth_impl.ts @@ -16,6 +16,7 @@ */ import { _FirebaseService, FirebaseApp } from '@firebase/app'; +import { Provider } from '@firebase/component'; import { Auth, AuthErrorMap, @@ -103,6 +104,7 @@ export class AuthImpl implements AuthInternal, _FirebaseService { constructor( public readonly app: FirebaseApp, + private readonly heartbeatServiceProvider: Provider<'heartbeat'>, public readonly config: ConfigInternal ) { this.name = app.name; @@ -583,9 +585,18 @@ export class AuthImpl implements AuthInternal, _FirebaseService { const headers: Record = { [HttpHeader.X_CLIENT_VERSION]: this.clientVersion, }; + if (this.app.options.appId) { headers[HttpHeader.X_FIREBASE_GMPID] = this.app.options.appId; } + + // If the heartbeat service exists, add the heartbeat string + const heartbeatsHeader = await this.heartbeatServiceProvider.getImmediate({ + optional: true, + })?.getHeartbeatsHeader(); + if (heartbeatsHeader) { + headers[HttpHeader.X_FIREBASE_CLIENT] = heartbeatsHeader; + } return headers; } } diff --git a/packages/auth/src/core/auth/register.ts b/packages/auth/src/core/auth/register.ts index abff9c25db0..a8439bc418a 100644 --- a/packages/auth/src/core/auth/register.ts +++ b/packages/auth/src/core/auth/register.ts @@ -19,7 +19,7 @@ import { _registerComponent, registerVersion } from '@firebase/app'; import { Component, ComponentType, - InstantiationMode + InstantiationMode, } from '@firebase/component'; import { name, version } from '../../../package.json'; @@ -61,8 +61,9 @@ export function registerAuth(clientPlatform: ClientPlatform): void { _ComponentName.AUTH, (container, { options: deps }: { options?: Dependencies }) => { const app = container.getProvider('app').getImmediate()!; + const heartbeatServiceProvider = container.getProvider<'heartbeat'>('heartbeat'); const { apiKey, authDomain } = app.options; - return (app => { + return ((app, heartbeatServiceProvider) => { _assert( apiKey && !apiKey.includes(':'), AuthErrorCode.INVALID_API_KEY, @@ -82,11 +83,11 @@ export function registerAuth(clientPlatform: ClientPlatform): void { sdkClientVersion: _getClientVersion(clientPlatform) }; - const authInstance = new AuthImpl(app, config); + const authInstance = new AuthImpl(app, heartbeatServiceProvider, config); _initializeAuthInstance(authInstance, deps); return authInstance; - })(app); + })(app, heartbeatServiceProvider); }, ComponentType.PUBLIC ) diff --git a/packages/auth/src/platform_browser/auth.test.ts b/packages/auth/src/platform_browser/auth.test.ts index fd4aac82df8..ad104dfceb7 100644 --- a/packages/auth/src/platform_browser/auth.test.ts +++ b/packages/auth/src/platform_browser/auth.test.ts @@ -28,7 +28,7 @@ import { } from '../model/public_types'; import { OperationType } from '../model/enums'; -import { testAuth, testUser } from '../../test/helpers/mock_auth'; +import { FAKE_HEARTBEAT_CONTROLLER_PROVIDER, testAuth, testUser } from '../../test/helpers/mock_auth'; import { AuthImpl, DefaultConfig } from '../core/auth/auth_impl'; import { _initializeAuthInstance } from '../core/auth/initialize'; import { AuthErrorCode } from '../core/errors'; @@ -66,7 +66,7 @@ describe('core/auth/auth_impl', () => { beforeEach(async () => { persistenceStub = sinon.stub(_getInstance(inMemoryPersistence)); - const authImpl = new AuthImpl(FAKE_APP, { + const authImpl = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -132,7 +132,7 @@ describe('core/auth/initializeAuth', () => { popupRedirectResolver?: PopupRedirectResolver, authDomain = FAKE_APP.options.authDomain ): Promise { - const auth = new AuthImpl(FAKE_APP, { + const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, @@ -359,7 +359,7 @@ describe('core/auth/initializeAuth', () => { // Manually initialize auth to make sure no error is thrown, // since the _initializeAuthInstance function floats - const auth = new AuthImpl(FAKE_APP, { + const auth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: FAKE_APP.options.apiKey!, apiHost: DefaultConfig.API_HOST, apiScheme: DefaultConfig.API_SCHEME, diff --git a/packages/auth/test/helpers/mock_auth.ts b/packages/auth/test/helpers/mock_auth.ts index 459621cd77d..5b85e162933 100644 --- a/packages/auth/test/helpers/mock_auth.ts +++ b/packages/auth/test/helpers/mock_auth.ts @@ -16,6 +16,7 @@ */ import { FirebaseApp } from '@firebase/app'; +import { Provider } from '@firebase/component'; import { PopupRedirectResolver } from '../../src/model/public_types'; import { debugErrorMap } from '../../src'; @@ -44,6 +45,16 @@ const FAKE_APP: FirebaseApp = { automaticDataCollectionEnabled: false }; +export const FAKE_HEARTBEAT_CONTROLLER = { + getHeartbeatsHeader: async () => '', +}; + +export const FAKE_HEARTBEAT_CONTROLLER_PROVIDER: Provider<'heartbeat'> = { + getImmediate(): typeof FAKE_HEARTBEAT_CONTROLLER { + return FAKE_HEARTBEAT_CONTROLLER; + } +} as unknown as Provider<'heartbeat'>; + export class MockPersistenceLayer extends InMemoryPersistence { lastObjectSet: PersistedBlob | null = null; @@ -62,7 +73,7 @@ export async function testAuth( popupRedirectResolver?: PopupRedirectResolver, persistence = new MockPersistenceLayer() ): Promise { - const auth: TestAuth = new AuthImpl(FAKE_APP, { + const auth: TestAuth = new AuthImpl(FAKE_APP, FAKE_HEARTBEAT_CONTROLLER_PROVIDER, { apiKey: TEST_KEY, authDomain: TEST_AUTH_DOMAIN, apiHost: TEST_HOST, From efe11b756efeb07b3c9d1b488f078b24aa7ccb9c Mon Sep 17 00:00:00 2001 From: Sam Olsen Date: Fri, 25 Feb 2022 15:17:10 -0800 Subject: [PATCH 2/3] Undo worker commenting out --- packages/auth/demo/src/index.js | 78 ++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/auth/demo/src/index.js b/packages/auth/demo/src/index.js index 0e3bf6ac93f..884110d3408 100644 --- a/packages/auth/demo/src/index.js +++ b/packages/auth/demo/src/index.js @@ -1750,45 +1750,45 @@ function initApp() { } // Install servicerWorker if supported. - // if ('serviceWorker' in navigator) { - // navigator.serviceWorker - // .register('/service-worker.js') - // .then(reg => { - // // Registration worked. - // console.log('Registration succeeded. Scope is ' + reg.scope); - // }) - // .catch(error => { - // // Registration failed. - // console.log('Registration failed with ' + error.message); - // }); - // } - - // if (window.Worker) { - // webWorker = new Worker('/web-worker.js'); - // /** - // * Handles the incoming message from the web worker. - // * @param {!Object} e The message event received. - // */ - // webWorker.onmessage = function (e) { - // console.log('User data passed through web worker: ', e.data); - // switch (e.data.type) { - // case 'GET_USER_INFO': - // alertSuccess( - // 'User data passed through web worker: ' + JSON.stringify(e.data) - // ); - // break; - // case 'RUN_TESTS': - // if (e.data.status === 'success') { - // alertSuccess('Web worker tests ran successfully!'); - // } else { - // alertError('Error: ' + JSON.stringify(e.data.error)); - // } - // break; - // default: - // return; - // } - // }; - // } + if ('serviceWorker' in navigator) { + navigator.serviceWorker + .register('/service-worker.js') + .then(reg => { + // Registration worked. + console.log('Registration succeeded. Scope is ' + reg.scope); + }) + .catch(error => { + // Registration failed. + console.log('Registration failed with ' + error.message); + }); + } + + if (window.Worker) { + webWorker = new Worker('/web-worker.js'); + /** + * Handles the incoming message from the web worker. + * @param {!Object} e The message event received. + */ + webWorker.onmessage = function (e) { + console.log('User data passed through web worker: ', e.data); + switch (e.data.type) { + case 'GET_USER_INFO': + alertSuccess( + 'User data passed through web worker: ' + JSON.stringify(e.data) + ); + break; + case 'RUN_TESTS': + if (e.data.status === 'success') { + alertSuccess('Web worker tests ran successfully!'); + } else { + alertError('Error: ' + JSON.stringify(e.data.error)); + } + break; + default: + return; + } + }; + } /** * Asks the web worker, if supported in current browser, to return the user info From 3d721a5e962d1c54c59bb89145d3a24e185ff317 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 25 Feb 2022 15:20:47 -0800 Subject: [PATCH 3/3] Changeset --- .changeset/neat-olives-punch.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/neat-olives-punch.md diff --git a/.changeset/neat-olives-punch.md b/.changeset/neat-olives-punch.md new file mode 100644 index 00000000000..9ae7caec82a --- /dev/null +++ b/.changeset/neat-olives-punch.md @@ -0,0 +1,6 @@ +--- +"@firebase/auth-compat": patch +"@firebase/auth": patch +--- + +Heartbeat