From 5965fbeb2b98cd138e7197ea7a2a1a72d481d855 Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Mon, 14 Sep 2020 17:09:31 -0700 Subject: [PATCH 1/6] Initial copy of performance and performance-types to packages-exp --- packages-exp/performance-exp/.eslintrc.js | 26 ++ packages-exp/performance-exp/CHANGELOG.md | 53 +++ packages-exp/performance-exp/README.md | 5 + packages-exp/performance-exp/index.ts | 83 ++++ packages-exp/performance-exp/karma.conf.js | 34 ++ packages-exp/performance-exp/package.json | 59 +++ packages-exp/performance-exp/rollup.config.js | 69 ++++ packages-exp/performance-exp/src/constants.ts | 42 ++ .../src/controllers/perf.test.ts | 137 +++++++ .../performance-exp/src/controllers/perf.ts | 63 +++ .../src/resources/network_request.test.ts | 78 ++++ .../src/resources/network_request.ts | 77 ++++ .../src/resources/trace.test.ts | 260 ++++++++++++ .../performance-exp/src/resources/trace.ts | 329 +++++++++++++++ .../src/services/api_service.test.ts | 113 ++++++ .../src/services/api_service.ts | 163 ++++++++ .../src/services/iid_service.test.ts | 60 +++ .../src/services/iid_service.ts | 47 +++ .../services/initialization_service.test.ts | 65 +++ .../src/services/initialization_service.ts | 77 ++++ .../services/oob_resources_service.test.ts | 199 +++++++++ .../src/services/oob_resources_service.ts | 93 +++++ .../src/services/perf_logger.test.ts | 383 ++++++++++++++++++ .../src/services/perf_logger.ts | 243 +++++++++++ .../services/remote_config_service.test.ts | 274 +++++++++++++ .../src/services/remote_config_service.ts | 233 +++++++++++ .../src/services/settings_service.ts | 107 +++++ .../src/services/transport_service.test.ts | 113 ++++++ .../src/services/transport_service.ts | 190 +++++++++ .../src/utils/attribute_utils.test.ts | 215 ++++++++++ .../src/utils/attributes_utils.ts | 117 ++++++ .../src/utils/console_logger.ts | 22 + .../performance-exp/src/utils/errors.ts | 70 ++++ .../src/utils/metric_utils.test.ts | 93 +++++ .../performance-exp/src/utils/metric_utils.ts | 64 +++ .../src/utils/string_merger.test.ts | 54 +++ .../src/utils/string_merger.ts | 35 ++ packages-exp/performance-exp/test/setup.ts | 27 ++ packages-exp/performance-exp/tsconfig.json | 12 + packages-exp/performance-types-exp/index.d.ts | 119 ++++++ .../performance-types-exp/package.json | 25 ++ .../performance-types-exp/tsconfig.json | 9 + 42 files changed, 4537 insertions(+) create mode 100644 packages-exp/performance-exp/.eslintrc.js create mode 100644 packages-exp/performance-exp/CHANGELOG.md create mode 100644 packages-exp/performance-exp/README.md create mode 100644 packages-exp/performance-exp/index.ts create mode 100644 packages-exp/performance-exp/karma.conf.js create mode 100644 packages-exp/performance-exp/package.json create mode 100644 packages-exp/performance-exp/rollup.config.js create mode 100644 packages-exp/performance-exp/src/constants.ts create mode 100644 packages-exp/performance-exp/src/controllers/perf.test.ts create mode 100644 packages-exp/performance-exp/src/controllers/perf.ts create mode 100644 packages-exp/performance-exp/src/resources/network_request.test.ts create mode 100644 packages-exp/performance-exp/src/resources/network_request.ts create mode 100644 packages-exp/performance-exp/src/resources/trace.test.ts create mode 100644 packages-exp/performance-exp/src/resources/trace.ts create mode 100644 packages-exp/performance-exp/src/services/api_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/api_service.ts create mode 100644 packages-exp/performance-exp/src/services/iid_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/iid_service.ts create mode 100644 packages-exp/performance-exp/src/services/initialization_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/initialization_service.ts create mode 100644 packages-exp/performance-exp/src/services/oob_resources_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/oob_resources_service.ts create mode 100644 packages-exp/performance-exp/src/services/perf_logger.test.ts create mode 100644 packages-exp/performance-exp/src/services/perf_logger.ts create mode 100644 packages-exp/performance-exp/src/services/remote_config_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/remote_config_service.ts create mode 100644 packages-exp/performance-exp/src/services/settings_service.ts create mode 100644 packages-exp/performance-exp/src/services/transport_service.test.ts create mode 100644 packages-exp/performance-exp/src/services/transport_service.ts create mode 100644 packages-exp/performance-exp/src/utils/attribute_utils.test.ts create mode 100644 packages-exp/performance-exp/src/utils/attributes_utils.ts create mode 100644 packages-exp/performance-exp/src/utils/console_logger.ts create mode 100644 packages-exp/performance-exp/src/utils/errors.ts create mode 100644 packages-exp/performance-exp/src/utils/metric_utils.test.ts create mode 100644 packages-exp/performance-exp/src/utils/metric_utils.ts create mode 100644 packages-exp/performance-exp/src/utils/string_merger.test.ts create mode 100644 packages-exp/performance-exp/src/utils/string_merger.ts create mode 100644 packages-exp/performance-exp/test/setup.ts create mode 100644 packages-exp/performance-exp/tsconfig.json create mode 100644 packages-exp/performance-types-exp/index.d.ts create mode 100644 packages-exp/performance-types-exp/package.json create mode 100644 packages-exp/performance-types-exp/tsconfig.json diff --git a/packages-exp/performance-exp/.eslintrc.js b/packages-exp/performance-exp/.eslintrc.js new file mode 100644 index 00000000000..ca80aa0f69a --- /dev/null +++ b/packages-exp/performance-exp/.eslintrc.js @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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. + */ + +module.exports = { + extends: '../../config/.eslintrc.js', + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + } +}; diff --git a/packages-exp/performance-exp/CHANGELOG.md b/packages-exp/performance-exp/CHANGELOG.md new file mode 100644 index 00000000000..ddfcb314890 --- /dev/null +++ b/packages-exp/performance-exp/CHANGELOG.md @@ -0,0 +1,53 @@ +# @firebase/performance + +## 0.4.1 + +### Patch Changes + +- Updated dependencies [[`da1c7df79`](https://github.com/firebase/firebase-js-sdk/commit/da1c7df7982b08bbef82fcc8d93255f3e2d23cca), [`fb3b095e4`](https://github.com/firebase/firebase-js-sdk/commit/fb3b095e4b7c8f57fdb3172bc039c84576abf290)]: + - @firebase/component@0.1.19 + - @firebase/util@0.3.2 + - @firebase/installations@0.4.17 + +## 0.4.0 + +### Minor Changes + +- [`67501b980`](https://github.com/firebase/firebase-js-sdk/commit/67501b9806c7014738080bc0be945b2c0748c17e) [#3424](https://github.com/firebase/firebase-js-sdk/pull/3424) - Issue 2393 - Add environment check to Performance Module + +## 0.3.11 + +### Patch Changes + +- Updated dependencies [[`d4ca3da0`](https://github.com/firebase/firebase-js-sdk/commit/d4ca3da0a59fcea1261ba69d7eb663bba38d3089)]: + - @firebase/util@0.3.1 + - @firebase/component@0.1.18 + - @firebase/installations@0.4.16 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [[`a87676b8`](https://github.com/firebase/firebase-js-sdk/commit/a87676b84b78ccc2f057a22eb947a5d13402949c)]: + - @firebase/util@0.3.0 + - @firebase/component@0.1.17 + - @firebase/installations@0.4.15 + +## 0.3.9 + +### Patch Changes + +- [`a754645e`](https://github.com/firebase/firebase-js-sdk/commit/a754645ec2be1b8c205f25f510196eee298b0d6e) [#3297](https://github.com/firebase/firebase-js-sdk/pull/3297) Thanks [@renovate](https://github.com/apps/renovate)! - Update dependency typescript to v3.9.5 + +- Updated dependencies [[`a754645e`](https://github.com/firebase/firebase-js-sdk/commit/a754645ec2be1b8c205f25f510196eee298b0d6e)]: + - @firebase/component@0.1.16 + - @firebase/installations@0.4.14 + - @firebase/logger@0.2.6 + +## 0.3.0 + +- [changed] Updated internal performance event transport mechanism. + +## 0.2.30 + +- [changed] Internal transport protocol update from proto2 to proto3. diff --git a/packages-exp/performance-exp/README.md b/packages-exp/performance-exp/README.md new file mode 100644 index 00000000000..5c83dbc51b7 --- /dev/null +++ b/packages-exp/performance-exp/README.md @@ -0,0 +1,5 @@ +# @firebase/performance + +This is the Firebase Performance component of the Firebase JS SDK. + +**This package is not intended for direct usage, and should only be used via the officially supported [firebase](https://www.npmjs.com/package/firebase) package.** diff --git a/packages-exp/performance-exp/index.ts b/packages-exp/performance-exp/index.ts new file mode 100644 index 00000000000..6829a9154a1 --- /dev/null +++ b/packages-exp/performance-exp/index.ts @@ -0,0 +1,83 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 firebase from '@firebase/app'; +import '@firebase/installations'; +import { FirebaseApp, FirebaseNamespace } from '@firebase/app-types'; +import { _FirebaseNamespace } from '@firebase/app-types/private'; +import { PerformanceController } from './src/controllers/perf'; +import { setupApi } from './src/services/api_service'; +import { SettingsService } from './src/services/settings_service'; +import { ERROR_FACTORY, ErrorCode } from './src/utils/errors'; +import { FirebasePerformance } from '@firebase/performance-types'; +import { Component, ComponentType } from '@firebase/component'; +import { FirebaseInstallations } from '@firebase/installations-types'; +import { name, version } from './package.json'; + +const DEFAULT_ENTRY_NAME = '[DEFAULT]'; + +export function registerPerformance(instance: FirebaseNamespace): void { + const factoryMethod = ( + app: FirebaseApp, + installations: FirebaseInstallations + ): PerformanceController => { + if (app.name !== DEFAULT_ENTRY_NAME) { + throw ERROR_FACTORY.create(ErrorCode.FB_NOT_DEFAULT); + } + if (typeof window === 'undefined') { + throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); + } + setupApi(window); + SettingsService.getInstance().firebaseAppInstance = app; + SettingsService.getInstance().installationsService = installations; + return new PerformanceController(app); + }; + + // Register performance with firebase-app. + (instance as _FirebaseNamespace).INTERNAL.registerComponent( + new Component( + 'performance', + container => { + /* Dependencies */ + // getImmediate for FirebaseApp will always succeed + const app = container.getProvider('app').getImmediate(); + // The following call will always succeed because perf has `import '@firebase/installations'` + const installations = container + .getProvider('installations') + .getImmediate(); + + return factoryMethod(app, installations); + }, + ComponentType.PUBLIC + ) + ); + + instance.registerVersion(name, version); +} + +registerPerformance(firebase); + +declare module '@firebase/app-types' { + interface FirebaseNamespace { + performance?: { + (app?: FirebaseApp): FirebasePerformance; + }; + } + interface FirebaseApp { + performance?(): FirebasePerformance; + } +} diff --git a/packages-exp/performance-exp/karma.conf.js b/packages-exp/performance-exp/karma.conf.js new file mode 100644 index 00000000000..7d6d24d6c2b --- /dev/null +++ b/packages-exp/performance-exp/karma.conf.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + +const karmaBase = require('../../config/karma.base'); + +const files = [`test/**/*`, 'src/**/*.test.ts']; + +module.exports = function (config) { + config.set({ + ...karmaBase, + // files to load into karma + files, + preprocessors: { '**/*.ts': ['webpack', 'sourcemap'] }, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }); +}; + +module.exports.files = files; diff --git a/packages-exp/performance-exp/package.json b/packages-exp/performance-exp/package.json new file mode 100644 index 00000000000..8bbc5df8a72 --- /dev/null +++ b/packages-exp/performance-exp/package.json @@ -0,0 +1,59 @@ +{ + "name": "@firebase/performance-exp", + "version": "0.4.1", + "description": "Firebase performance for web", + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.cjs.js", + "browser": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "esm2017": "dist/index.esm2017.js", + "files": ["dist"], + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts'", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/performance-exp --include-dependencies build", + "dev": "rollup -c -w", + "test": "run-p lint test:browser", + "test:ci": "node ../../scripts/run_tests_in_ci.js -s test:browser", + "test:browser": "karma start --single-run", + "test:debug": "karma start --browsers=Chrome --auto-watch", + "prepare": "yarn build", + "prettier": "prettier --write '{src,test}/**/*.{js,ts}'" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + }, + "dependencies": { + "@firebase/logger": "0.2.6", + "@firebase/installations": "0.4.17", + "@firebase/util": "0.3.2", + "@firebase/performance-types-exp": "0.0.13", + "@firebase/component": "0.1.19", + "tslib": "^1.11.1" + }, + "license": "Apache-2.0", + "devDependencies": { + "@firebase/app": "0.6.11", + "rollup": "2.26.7", + "rollup-plugin-json": "4.0.0", + "rollup-plugin-typescript2": "0.27.2", + "typescript": "4.0.2" + }, + "repository": { + "directory": "packages/performance-exp", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "typings": "dist/index.d.ts", + "nyc": { + "extension": [ + ".ts" + ], + "reportDir": "./coverage/node" + } +} diff --git a/packages-exp/performance-exp/rollup.config.js b/packages-exp/performance-exp/rollup.config.js new file mode 100644 index 00000000000..9cfe6294e74 --- /dev/null +++ b/packages-exp/performance-exp/rollup.config.js @@ -0,0 +1,69 @@ +/** + * @license + * Copyright 2018 Google LLC + * + * 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 json from 'rollup-plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import typescript from 'typescript'; +import pkg from './package.json'; + +const deps = Object.keys( + Object.assign({}, pkg.peerDependencies, pkg.dependencies) +); + +/** + * ES5 Builds + */ +const es5BuildPlugins = [typescriptPlugin({ typescript }), json()]; + +const es5Builds = [ + { + input: 'index.ts', + output: [ + { file: pkg.main, format: 'cjs', sourcemap: true }, + { file: pkg.module, format: 'es', sourcemap: true } + ], + plugins: es5BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +/** + * ES2017 Builds + */ + +const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + } + }), + json({ preferConst: true }) +]; + +const es2017Builds = [ + { + input: 'index.ts', + output: [{ file: pkg.esm2017, format: 'es', sourcemap: true }], + plugins: es2017BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/performance-exp/src/constants.ts b/packages-exp/performance-exp/src/constants.ts new file mode 100644 index 00000000000..b0566a2a8be --- /dev/null +++ b/packages-exp/performance-exp/src/constants.ts @@ -0,0 +1,42 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * 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 { version } from '../package.json'; + +export const SDK_VERSION = version; +/** The prefix for start User Timing marks used for creating Traces. */ +export const TRACE_START_MARK_PREFIX = 'FB-PERF-TRACE-START'; +/** The prefix for stop User Timing marks used for creating Traces. */ +export const TRACE_STOP_MARK_PREFIX = 'FB-PERF-TRACE-STOP'; +/** The prefix for User Timing measure used for creating Traces. */ +export const TRACE_MEASURE_PREFIX = 'FB-PERF-TRACE-MEASURE'; +/** The prefix for out of the box page load Trace name. */ +export const OOB_TRACE_PAGE_LOAD_PREFIX = '_wt_'; + +export const FIRST_PAINT_COUNTER_NAME = '_fp'; + +export const FIRST_CONTENTFUL_PAINT_COUNTER_NAME = '_fcp'; + +export const FIRST_INPUT_DELAY_COUNTER_NAME = '_fid'; + +export const CONFIG_LOCAL_STORAGE_KEY = '@firebase/performance/config'; + +export const CONFIG_EXPIRY_LOCAL_STORAGE_KEY = + '@firebase/performance/configexpire'; + +export const SERVICE = 'performance'; +export const SERVICE_NAME = 'Performance'; diff --git a/packages-exp/performance-exp/src/controllers/perf.test.ts b/packages-exp/performance-exp/src/controllers/perf.test.ts new file mode 100644 index 00000000000..6d3a24e723f --- /dev/null +++ b/packages-exp/performance-exp/src/controllers/perf.test.ts @@ -0,0 +1,137 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { stub } from 'sinon'; +import { PerformanceController } from '../controllers/perf'; +import { Trace } from '../resources/trace'; +import { Api, setupApi } from '../services/api_service'; +import { FirebaseApp } from '@firebase/app-types'; +import * as initializationService from '../services/initialization_service'; +import * as FirebaseUtil from '@firebase/util'; +import { consoleLogger } from '../utils/console_logger'; +import '../../test/setup'; + +describe('Firebase Performance Test', () => { + setupApi(window); + + const fakeFirebaseConfig = { + apiKey: 'api-key', + authDomain: 'project-id.firebaseapp.com', + databaseURL: 'https://project-id.firebaseio.com', + projectId: 'project-id', + storageBucket: 'project-id.appspot.com', + messagingSenderId: 'sender-id', + appId: '1:111:web:a1234' + }; + + const fakeFirebaseApp = ({ + options: fakeFirebaseConfig + } as unknown) as FirebaseApp; + + describe('#constructor', () => { + it('does not initialize performance if the required apis are not available', () => { + stub(Api.prototype, 'requiredApisAvailable').returns(false); + stub(initializationService, 'getInitializationPromise'); + new PerformanceController(fakeFirebaseApp); + expect(initializationService.getInitializationPromise).not.be.called; + }); + it('does not initialize performance if validateIndexedDBOpenable return false', async () => { + stub(Api.prototype, 'requiredApisAvailable').returns(true); + const validateStub = stub( + FirebaseUtil, + 'validateIndexedDBOpenable' + ).resolves(false); + stub(initializationService, 'getInitializationPromise'); + new PerformanceController(fakeFirebaseApp); + await validateStub; + expect(initializationService.getInitializationPromise).not.be.called; + }); + + it('does not initialize performance if validateIndexedDBOpenable throws an error', async () => { + stub(Api.prototype, 'requiredApisAvailable').returns(true); + const validateStub = stub( + FirebaseUtil, + 'validateIndexedDBOpenable' + ).rejects(); + + stub(initializationService, 'getInitializationPromise'); + stub(consoleLogger, 'info'); + new PerformanceController(fakeFirebaseApp); + try { + await validateStub; + expect(initializationService.getInitializationPromise).not.be.called; + expect(consoleLogger.info).be.called; + } catch (ignored) {} + }); + }); + + describe('#trace', () => { + it('creates a custom trace', () => { + const controller = new PerformanceController(fakeFirebaseApp); + const myTrace = controller.trace('myTrace'); + + expect(myTrace).to.be.instanceOf(Trace); + }); + + it('custom trace has the correct name', () => { + const controller = new PerformanceController(fakeFirebaseApp); + const myTrace = controller.trace('myTrace'); + + expect(myTrace.name).is.equal('myTrace'); + }); + + it('custom trace is not auto', () => { + const controller = new PerformanceController(fakeFirebaseApp); + const myTrace = controller.trace('myTrace'); + + expect(myTrace.isAuto).is.equal(false); + }); + }); + + describe('#instrumentationEnabled', () => { + it('sets instrumentationEnabled to enabled', async () => { + const controller = new PerformanceController(fakeFirebaseApp); + + controller.instrumentationEnabled = true; + expect(controller.instrumentationEnabled).is.equal(true); + }); + + it('sets instrumentationEnabled to disabled', async () => { + const controller = new PerformanceController(fakeFirebaseApp); + + controller.instrumentationEnabled = false; + expect(controller.instrumentationEnabled).is.equal(false); + }); + }); + + describe('#dataCollectionEnabled', () => { + it('sets dataCollectionEnabled to enabled', async () => { + const controller = new PerformanceController(fakeFirebaseApp); + + controller.dataCollectionEnabled = true; + expect(controller.dataCollectionEnabled).is.equal(true); + }); + + it('sets dataCollectionEnabled to disabled', () => { + const controller = new PerformanceController(fakeFirebaseApp); + + controller.dataCollectionEnabled = false; + expect(controller.dataCollectionEnabled).is.equal(false); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/controllers/perf.ts b/packages-exp/performance-exp/src/controllers/perf.ts new file mode 100644 index 00000000000..9793f614dd4 --- /dev/null +++ b/packages-exp/performance-exp/src/controllers/perf.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { Trace } from '../resources/trace'; +import { setupOobResources } from '../services/oob_resources_service'; +import { SettingsService } from '../services/settings_service'; +import { getInitializationPromise } from '../services/initialization_service'; +import { Api } from '../services/api_service'; +import { FirebaseApp } from '@firebase/app-types'; +import { FirebasePerformance } from '@firebase/performance-types'; +import { setupTransportService } from '../services/transport_service'; +import { validateIndexedDBOpenable } from '@firebase/util'; +import { consoleLogger } from '../utils/console_logger'; +export class PerformanceController implements FirebasePerformance { + constructor(readonly app: FirebaseApp) { + if (Api.getInstance().requiredApisAvailable()) { + validateIndexedDBOpenable() + .then(isAvailable => { + if (isAvailable) { + setupTransportService(); + getInitializationPromise().then( + setupOobResources, + setupOobResources + ); + } + }) + .catch(error => { + consoleLogger.info(`Environment doesn't support IndexedDB: ${error}`); + }); + } + } + + trace(name: string): Trace { + return new Trace(name); + } + + set instrumentationEnabled(val: boolean) { + SettingsService.getInstance().instrumentationEnabled = val; + } + get instrumentationEnabled(): boolean { + return SettingsService.getInstance().instrumentationEnabled; + } + + set dataCollectionEnabled(val: boolean) { + SettingsService.getInstance().dataCollectionEnabled = val; + } + get dataCollectionEnabled(): boolean { + return SettingsService.getInstance().dataCollectionEnabled; + } +} diff --git a/packages-exp/performance-exp/src/resources/network_request.test.ts b/packages-exp/performance-exp/src/resources/network_request.test.ts new file mode 100644 index 00000000000..2484b08fdf2 --- /dev/null +++ b/packages-exp/performance-exp/src/resources/network_request.test.ts @@ -0,0 +1,78 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub, restore } from 'sinon'; +import { createNetworkRequestEntry } from '../../src/resources/network_request'; +import { expect } from 'chai'; +import { Api, setupApi } from '../services/api_service'; +import * as perfLogger from '../services/perf_logger'; + +import '../../test/setup'; + +describe('Firebase Performance > network_request', () => { + setupApi(window); + + beforeEach(() => { + stub(Api.prototype, 'getTimeOrigin').returns(1528521843799.5032); + stub(perfLogger, 'logNetworkRequest'); + }); + + afterEach(() => { + restore(); + }); + + describe('#createNetworkRequestEntry', () => { + it('logs network request when all required fields present', () => { + const PERFORMANCE_ENTRY = ({ + name: 'http://some.test.website.com', + transferSize: 500, + startTime: 1645352.632345, + responseStart: 1645360.244323, + responseEnd: 1645360.832443 + } as unknown) as PerformanceResourceTiming; + + const EXPECTED_NETWORK_REQUEST = { + url: 'http://some.test.website.com', + responsePayloadBytes: 500, + startTimeUs: 1528523489152135, + timeToResponseInitiatedUs: 7611, + timeToResponseCompletedUs: 8200 + }; + + createNetworkRequestEntry(PERFORMANCE_ENTRY); + + expect( + (perfLogger.logNetworkRequest as any).calledWith( + EXPECTED_NETWORK_REQUEST + ) + ).to.be.true; + }); + + it('doesnt log network request when responseStart is absent', () => { + const PERFORMANCE_ENTRY = ({ + name: 'http://some.test.website.com', + transferSize: 500, + startTime: 1645352.632345, + responseEnd: 1645360.832443 + } as unknown) as PerformanceResourceTiming; + + createNetworkRequestEntry(PERFORMANCE_ENTRY); + + expect(perfLogger.logNetworkRequest).to.not.have.been.called; + }); + }); +}); diff --git a/packages-exp/performance-exp/src/resources/network_request.ts b/packages-exp/performance-exp/src/resources/network_request.ts new file mode 100644 index 00000000000..ac7cfe114fe --- /dev/null +++ b/packages-exp/performance-exp/src/resources/network_request.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { Api } from '../services/api_service'; +import { logNetworkRequest } from '../services/perf_logger'; + +// The order of values of this enum should not be changed. +export const enum HttpMethod { + HTTP_METHOD_UNKNOWN = 0, + GET = 1, + PUT = 2, + POST = 3, + DELETE = 4, + HEAD = 5, + PATCH = 6, + OPTIONS = 7, + TRACE = 8, + CONNECT = 9 +} + +// Durations are in microseconds. +export interface NetworkRequest { + url: string; + httpMethod?: HttpMethod; + requestPayloadBytes?: number; + responsePayloadBytes?: number; + httpResponseCode?: number; + responseContentType?: string; + startTimeUs?: number; + timeToRequestCompletedUs?: number; + timeToResponseInitiatedUs?: number; + timeToResponseCompletedUs?: number; +} + +export function createNetworkRequestEntry(entry: PerformanceEntry): void { + const performanceEntry = entry as PerformanceResourceTiming; + if (!performanceEntry || performanceEntry.responseStart === undefined) { + return; + } + const timeOrigin = Api.getInstance().getTimeOrigin(); + const startTimeUs = Math.floor( + (performanceEntry.startTime + timeOrigin) * 1000 + ); + const timeToResponseInitiatedUs = performanceEntry.responseStart + ? Math.floor( + (performanceEntry.responseStart - performanceEntry.startTime) * 1000 + ) + : undefined; + const timeToResponseCompletedUs = Math.floor( + (performanceEntry.responseEnd - performanceEntry.startTime) * 1000 + ); + // Remove the query params from logged network request url. + const url = performanceEntry.name && performanceEntry.name.split('?')[0]; + const networkRequest: NetworkRequest = { + url, + responsePayloadBytes: performanceEntry.transferSize, + startTimeUs, + timeToResponseInitiatedUs, + timeToResponseCompletedUs + }; + + logNetworkRequest(networkRequest); +} diff --git a/packages-exp/performance-exp/src/resources/trace.test.ts b/packages-exp/performance-exp/src/resources/trace.test.ts new file mode 100644 index 00000000000..f9df8565443 --- /dev/null +++ b/packages-exp/performance-exp/src/resources/trace.test.ts @@ -0,0 +1,260 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { spy, stub, restore } from 'sinon'; +import { Trace } from '../resources/trace'; +import { expect } from 'chai'; +import { Api, setupApi } from '../services/api_service'; +import * as perfLogger from '../services/perf_logger'; + +import '../../test/setup'; + +describe('Firebase Performance > trace', () => { + setupApi(window); + let trace: Trace; + const createTrace = (): Trace => { + return new Trace('test'); + }; + + beforeEach(() => { + spy(Api.prototype, 'mark'); + stub(perfLogger, 'logTrace'); + trace = createTrace(); + }); + + afterEach(() => { + restore(); + }); + + describe('#start', () => { + beforeEach(() => { + trace.start(); + }); + + it('uses the underlying api method', () => { + expect(Api.getInstance().mark).to.be.calledOnce; + }); + + it('throws if a trace is started twice', () => { + expect(() => trace.start()).to.throw(); + }); + }); + + describe('#stop', () => { + it('adds a mark to the performance timeline', () => { + trace.start(); + trace.stop(); + + expect(Api.getInstance().mark).to.be.calledTwice; + }); + + it('logs the trace', () => { + trace.start(); + trace.stop(); + + expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; + }); + }); + + describe('#record', () => { + it('logs a trace without metrics or custom attributes', () => { + trace.record(1, 20); + + expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; + }); + + it('logs a trace with metrics', () => { + trace.record(1, 20, { metrics: { cacheHits: 1 } }); + + expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; + expect(trace.getMetric('cacheHits')).to.eql(1); + }); + + it('logs a trace with custom attributes', () => { + trace.record(1, 20, { attributes: { level: '1' } }); + + expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; + expect(trace.getAttributes()).to.eql({ level: '1' }); + }); + + it('logs a trace with custom attributes and metrics', () => { + trace.record(1, 20, { + attributes: { level: '1' }, + metrics: { cacheHits: 1 } + }); + + expect((perfLogger.logTrace as any).calledOnceWith(trace)).to.be.true; + expect(trace.getAttributes()).to.eql({ level: '1' }); + expect(trace.getMetric('cacheHits')).to.eql(1); + }); + }); + + describe('#incrementMetric', () => { + it('creates new metric if one doesnt exist.', () => { + trace.incrementMetric('cacheHits', 200); + + expect(trace.getMetric('cacheHits')).to.eql(200); + }); + + it('increments metric if it already exists.', () => { + trace.incrementMetric('cacheHits', 200); + trace.incrementMetric('cacheHits', 400); + + expect(trace.getMetric('cacheHits')).to.eql(600); + }); + + it('increments metric value as an integer even if the value is provided in float.', () => { + trace.incrementMetric('cacheHits', 200); + trace.incrementMetric('cacheHits', 400.38); + + expect(trace.getMetric('cacheHits')).to.eql(600); + }); + + it('increments metric value with a negative float.', () => { + trace.incrementMetric('cacheHits', 200); + trace.incrementMetric('cacheHits', -230.38); + + expect(trace.getMetric('cacheHits')).to.eql(-31); + }); + + it('throws error if metric doesnt exist and has invalid name', () => { + expect(() => trace.incrementMetric('_invalidMetric', 1)).to.throw(); + }); + }); + + describe('#putMetric', () => { + it('creates new metric if one doesnt exist and has valid name.', () => { + trace.putMetric('cacheHits', 200); + + expect(trace.getMetric('cacheHits')).to.eql(200); + }); + + it('sets the metric value as an integer even if the value is provided in float.', () => { + trace.putMetric('timelapse', 200.48); + + expect(trace.getMetric('timelapse')).to.eql(200); + }); + + it('replaces metric if it already exists.', () => { + trace.putMetric('cacheHits', 200); + trace.putMetric('cacheHits', 400); + + expect(trace.getMetric('cacheHits')).to.eql(400); + }); + + it('throws error if metric doesnt exist and has invalid name', () => { + expect(() => trace.putMetric('_invalidMetric', 1)).to.throw(); + expect(() => trace.putMetric('_fid', 1)).to.throw(); + }); + }); + + describe('#getMetric', () => { + it('returns 0 if metric doesnt exist', () => { + expect(trace.getMetric('doesThisExist')).to.equal(0); + }); + + it('returns 0 if it exists and equals 0', () => { + trace.putMetric('cacheHits', 0); + + expect(trace.getMetric('cacheHits')).to.equal(0); + }); + + it('returns metric if it exists', () => { + trace.putMetric('cacheHits', 200); + + expect(trace.getMetric('cacheHits')).to.equal(200); + }); + + it('returns multiple metrics if they exist', () => { + trace.putMetric('cacheHits', 200); + trace.putMetric('bytesDownloaded', 25); + + expect(trace.getMetric('cacheHits')).to.equal(200); + expect(trace.getMetric('bytesDownloaded')).to.equal(25); + }); + }); + + describe('#putAttribute', () => { + it('creates new attribute if it doesnt exist', () => { + trace.putAttribute('level', '4'); + + expect(trace.getAttributes()).to.eql({ level: '4' }); + }); + + it('replaces attribute if it exists', () => { + trace.putAttribute('level', '4'); + trace.putAttribute('level', '7'); + + expect(trace.getAttributes()).to.eql({ level: '7' }); + }); + + it('throws error if attribute name is invalid', () => { + expect(() => trace.putAttribute('_invalidAttribute', '1')).to.throw(); + }); + + it('throws error if attribute value is invalid', () => { + const longAttributeValue = + 'too-long-attribute-value-over-one-hundred-characters-too-long-attribute-value-over-one-' + + 'hundred-charac'; + expect(() => + trace.putAttribute('validName', longAttributeValue) + ).to.throw(); + }); + }); + + describe('#getAttribute', () => { + it('returns undefined for attribute that doesnt exist', () => { + expect(trace.getAttribute('level')).to.be.undefined; + }); + + it('returns attribute if it exists', () => { + trace.putAttribute('level', '4'); + expect(trace.getAttribute('level')).to.equal('4'); + }); + + it('returns separate attributes if they exist', () => { + trace.putAttribute('level', '4'); + trace.putAttribute('stage', 'beginning'); + + expect(trace.getAttribute('level')).to.equal('4'); + expect(trace.getAttribute('stage')).to.equal('beginning'); + }); + }); + + describe('#removeAttribute', () => { + it('does not throw if removing attribute that doesnt exist', () => { + expect(() => trace.removeAttribute('doesNotExist')).to.not.throw; + }); + + it('removes attribute if it exists', () => { + trace.putAttribute('level', '4'); + expect(trace.getAttribute('level')).to.equal('4'); + + trace.removeAttribute('level'); + expect(trace.getAttribute('level')).to.be.undefined; + }); + + it('retains other attributes', () => { + trace.putAttribute('level', '4'); + trace.putAttribute('stage', 'beginning'); + + trace.removeAttribute('level'); + expect(trace.getAttribute('level')).to.be.undefined; + expect(trace.getAttribute('stage')).to.equal('beginning'); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/resources/trace.ts b/packages-exp/performance-exp/src/resources/trace.ts new file mode 100644 index 00000000000..be34129690c --- /dev/null +++ b/packages-exp/performance-exp/src/resources/trace.ts @@ -0,0 +1,329 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { + TRACE_START_MARK_PREFIX, + TRACE_STOP_MARK_PREFIX, + TRACE_MEASURE_PREFIX, + OOB_TRACE_PAGE_LOAD_PREFIX, + FIRST_PAINT_COUNTER_NAME, + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + FIRST_INPUT_DELAY_COUNTER_NAME +} from '../constants'; +import { Api } from '../services/api_service'; +import { logTrace } from '../services/perf_logger'; +import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; +import { + isValidCustomAttributeName, + isValidCustomAttributeValue +} from '../utils/attributes_utils'; +import { + isValidMetricName, + convertMetricValueToInteger +} from '../utils/metric_utils'; +import { PerformanceTrace } from '@firebase/performance-types'; + +const enum TraceState { + UNINITIALIZED = 1, + RUNNING, + TERMINATED +} + +export class Trace implements PerformanceTrace { + private state: TraceState = TraceState.UNINITIALIZED; + startTimeUs!: number; + durationUs!: number; + private customAttributes: { [key: string]: string } = {}; + counters: { [counterName: string]: number } = {}; + private api = Api.getInstance(); + private randomId = Math.floor(Math.random() * 1000000); + private traceStartMark!: string; + private traceStopMark!: string; + private traceMeasure!: string; + + /** + * @param name The name of the trace. + * @param isAuto If the trace is auto-instrumented. + * @param traceMeasureName The name of the measure marker in user timing specification. This field + * is only set when the trace is built for logging when the user directly uses the user timing + * api (performance.mark and performance.measure). + */ + constructor( + readonly name: string, + readonly isAuto = false, + traceMeasureName?: string + ) { + if (!this.isAuto) { + this.traceStartMark = `${TRACE_START_MARK_PREFIX}-${this.randomId}-${this.name}`; + this.traceStopMark = `${TRACE_STOP_MARK_PREFIX}-${this.randomId}-${this.name}`; + this.traceMeasure = + traceMeasureName || + `${TRACE_MEASURE_PREFIX}-${this.randomId}-${this.name}`; + + if (traceMeasureName) { + // For the case of direct user timing traces, no start stop will happen. The measure object + // is already available. + this.calculateTraceMetrics(); + } + } + } + + /** + * Starts a trace. The measurement of the duration starts at this point. + */ + start(): void { + if (this.state !== TraceState.UNINITIALIZED) { + throw ERROR_FACTORY.create(ErrorCode.TRACE_STARTED_BEFORE, { + traceName: this.name + }); + } + this.api.mark(this.traceStartMark); + this.state = TraceState.RUNNING; + } + + /** + * Stops the trace. The measurement of the duration of the trace stops at this point and trace + * is logged. + */ + stop(): void { + if (this.state !== TraceState.RUNNING) { + throw ERROR_FACTORY.create(ErrorCode.TRACE_STOPPED_BEFORE, { + traceName: this.name + }); + } + this.state = TraceState.TERMINATED; + this.api.mark(this.traceStopMark); + this.api.measure( + this.traceMeasure, + this.traceStartMark, + this.traceStopMark + ); + this.calculateTraceMetrics(); + logTrace(this); + } + + /** + * Records a trace with predetermined values. If this method is used a trace is created and logged + * directly. No need to use start and stop methods. + * @param startTime Trace start time since epoch in millisec + * @param duration The duraction of the trace in millisec + * @param options An object which can optionally hold maps of custom metrics and custom attributes + */ + record( + startTime: number, + duration: number, + options?: { + metrics?: { [key: string]: number }; + attributes?: { [key: string]: string }; + } + ): void { + this.durationUs = Math.floor(duration * 1000); + this.startTimeUs = Math.floor(startTime * 1000); + if (options && options.attributes) { + this.customAttributes = { ...options.attributes }; + } + if (options && options.metrics) { + for (const metric of Object.keys(options.metrics)) { + if (!isNaN(Number(options.metrics[metric]))) { + this.counters[metric] = Number(Math.floor(options.metrics[metric])); + } + } + } + logTrace(this); + } + + /** + * Increments a custom metric by a certain number or 1 if number not specified. Will create a new + * custom metric if one with the given name does not exist. The value will be floored down to an + * integer. + * @param counter Name of the custom metric + * @param numAsInteger Increment by value + */ + incrementMetric(counter: string, numAsInteger = 1): void { + if (this.counters[counter] === undefined) { + this.putMetric(counter, numAsInteger); + } else { + this.putMetric(counter, this.counters[counter] + numAsInteger); + } + } + + /** + * Sets a custom metric to a specified value. Will create a new custom metric if one with the + * given name does not exist. The value will be floored down to an integer. + * @param counter Name of the custom metric + * @param numAsInteger Set custom metric to this value + */ + putMetric(counter: string, numAsInteger: number): void { + if (isValidMetricName(counter, this.name)) { + this.counters[counter] = convertMetricValueToInteger(numAsInteger); + } else { + throw ERROR_FACTORY.create(ErrorCode.INVALID_CUSTOM_METRIC_NAME, { + customMetricName: counter + }); + } + } + + /** + * Returns the value of the custom metric by that name. If a custom metric with that name does + * not exist will return zero. + * @param counter + */ + getMetric(counter: string): number { + return this.counters[counter] || 0; + } + + /** + * Sets a custom attribute of a trace to a certain value. + * @param attr + * @param value + */ + putAttribute(attr: string, value: string): void { + const isValidName = isValidCustomAttributeName(attr); + const isValidValue = isValidCustomAttributeValue(value); + if (isValidName && isValidValue) { + this.customAttributes[attr] = value; + return; + } + // Throw appropriate error when the attribute name or value is invalid. + if (!isValidName) { + throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_NAME, { + attributeName: attr + }); + } + if (!isValidValue) { + throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_VALUE, { + attributeValue: value + }); + } + } + + /** + * Retrieves the value a custom attribute of a trace is set to. + * @param attr + */ + getAttribute(attr: string): string | undefined { + return this.customAttributes[attr]; + } + + removeAttribute(attr: string): void { + if (this.customAttributes[attr] === undefined) { + return; + } + delete this.customAttributes[attr]; + } + + getAttributes(): { [key: string]: string } { + return { ...this.customAttributes }; + } + + private setStartTime(startTime: number): void { + this.startTimeUs = startTime; + } + + private setDuration(duration: number): void { + this.durationUs = duration; + } + + /** + * Calculates and assigns the duration and start time of the trace using the measure performance + * entry. + */ + private calculateTraceMetrics(): void { + const perfMeasureEntries = this.api.getEntriesByName(this.traceMeasure); + const perfMeasureEntry = perfMeasureEntries && perfMeasureEntries[0]; + if (perfMeasureEntry) { + this.durationUs = Math.floor(perfMeasureEntry.duration * 1000); + this.startTimeUs = Math.floor( + (perfMeasureEntry.startTime + this.api.getTimeOrigin()) * 1000 + ); + } + } + + /** + * @param navigationTimings A single element array which contains the navigationTIming object of + * the page load + * @param paintTimings A array which contains paintTiming object of the page load + * @param firstInputDelay First input delay in millisec + */ + static createOobTrace( + navigationTimings: PerformanceNavigationTiming[], + paintTimings: PerformanceEntry[], + firstInputDelay?: number + ): void { + const route = Api.getInstance().getUrl(); + if (!route) { + return; + } + const trace = new Trace(OOB_TRACE_PAGE_LOAD_PREFIX + route, true); + const timeOriginUs = Math.floor(Api.getInstance().getTimeOrigin() * 1000); + trace.setStartTime(timeOriginUs); + + // navigationTimings includes only one element. + if (navigationTimings && navigationTimings[0]) { + trace.setDuration(Math.floor(navigationTimings[0].duration * 1000)); + trace.putMetric( + 'domInteractive', + Math.floor(navigationTimings[0].domInteractive * 1000) + ); + trace.putMetric( + 'domContentLoadedEventEnd', + Math.floor(navigationTimings[0].domContentLoadedEventEnd * 1000) + ); + trace.putMetric( + 'loadEventEnd', + Math.floor(navigationTimings[0].loadEventEnd * 1000) + ); + } + + const FIRST_PAINT = 'first-paint'; + const FIRST_CONTENTFUL_PAINT = 'first-contentful-paint'; + if (paintTimings) { + const firstPaint = paintTimings.find( + paintObject => paintObject.name === FIRST_PAINT + ); + if (firstPaint && firstPaint.startTime) { + trace.putMetric( + FIRST_PAINT_COUNTER_NAME, + Math.floor(firstPaint.startTime * 1000) + ); + } + const firstContentfulPaint = paintTimings.find( + paintObject => paintObject.name === FIRST_CONTENTFUL_PAINT + ); + if (firstContentfulPaint && firstContentfulPaint.startTime) { + trace.putMetric( + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + Math.floor(firstContentfulPaint.startTime * 1000) + ); + } + + if (firstInputDelay) { + trace.putMetric( + FIRST_INPUT_DELAY_COUNTER_NAME, + Math.floor(firstInputDelay * 1000) + ); + } + } + + logTrace(trace); + } + + static createUserTimingTrace(measureName: string): void { + const trace = new Trace(measureName, false, measureName); + logTrace(trace); + } +} diff --git a/packages-exp/performance-exp/src/services/api_service.test.ts b/packages-exp/performance-exp/src/services/api_service.test.ts new file mode 100644 index 00000000000..32ac012ffc3 --- /dev/null +++ b/packages-exp/performance-exp/src/services/api_service.test.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub } from 'sinon'; +import { expect } from 'chai'; +import { Api, setupApi } from './api_service'; +import '../../test/setup'; +describe('Firebase Performance > api_service', () => { + const PAGE_URL = 'http://www.test.com/abcd?a=2'; + const PERFORMANCE_ENTRY: PerformanceEntry = { + duration: 0, + entryType: 'paint', + name: 'first-contentful-paint', + startTime: 149.01000005193055, + toJSON: () => {} + }; + + const mockWindow = { ...self }; + // hack for IE11. self.hasOwnProperty('performance') returns false in IE11 + mockWindow.performance = self.performance; + + let api: Api; + + beforeEach(() => { + stub(mockWindow.performance, 'mark'); + stub(mockWindow.performance, 'measure'); + stub(mockWindow.performance, 'getEntriesByType').returns([ + PERFORMANCE_ENTRY + ]); + stub(mockWindow.performance, 'getEntriesByName').returns([ + PERFORMANCE_ENTRY + ]); + // This is to make sure the test page is not changed by changing the href of location object. + mockWindow.location = { ...self.location, href: PAGE_URL }; + + setupApi(mockWindow); + api = Api.getInstance(); + }); + + describe('getUrl', () => { + it('removes the query params', () => { + expect(api.getUrl()).to.equal('http://www.test.com/abcd'); + }); + }); + + describe('mark', () => { + it('creates performance mark', () => { + const MARK_NAME = 'mark1'; + api.mark(MARK_NAME); + + expect(mockWindow.performance.mark).to.be.calledOnceWith(MARK_NAME); + }); + }); + + describe('measure', () => { + it('creates a performance measure', () => { + const MEASURE_NAME = 'measure1'; + const MARK_1_NAME = 'mark1'; + const MARK_2_NAME = 'mark2'; + api.measure(MEASURE_NAME, MARK_1_NAME, MARK_2_NAME); + + expect(mockWindow.performance.measure).to.be.calledOnceWith( + MEASURE_NAME, + MARK_1_NAME, + MARK_2_NAME + ); + }); + }); + + describe('getEntriesByType', () => { + it('calls the underlying performance api', () => { + expect(api.getEntriesByType('paint')).to.deep.equal([PERFORMANCE_ENTRY]); + }); + + it('does not throw if the browser does not include underlying api', () => { + api = new Api(({ performance: undefined } as unknown) as Window); + + expect(() => { + api.getEntriesByType('paint'); + }).to.not.throw(); + expect(api.getEntriesByType('paint')).to.deep.equal([]); + }); + }); + + describe('getEntriesByName', () => { + it('calls the underlying performance api', () => { + expect(api.getEntriesByName('paint')).to.deep.equal([PERFORMANCE_ENTRY]); + }); + + it('does not throw if the browser does not include underlying api', () => { + api = new Api(({ performance: undefined } as any) as Window); + + expect(() => { + api.getEntriesByName('paint'); + }).to.not.throw(); + expect(api.getEntriesByName('paint')).to.deep.equal([]); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/services/api_service.ts b/packages-exp/performance-exp/src/services/api_service.ts new file mode 100644 index 00000000000..df719e8fb16 --- /dev/null +++ b/packages-exp/performance-exp/src/services/api_service.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { ERROR_FACTORY, ErrorCode } from '../utils/errors'; +import { isIndexedDBAvailable } from '@firebase/util'; +import { consoleLogger } from '../utils/console_logger'; +declare global { + interface Window { + PerformanceObserver: typeof PerformanceObserver; + // eslint-disable-next-line @typescript-eslint/ban-types + perfMetrics?: { onFirstInputDelay: Function }; + } +} + +let apiInstance: Api | undefined; +let windowInstance: Window | undefined; + +export type EntryType = + | 'mark' + | 'measure' + | 'paint' + | 'resource' + | 'frame' + | 'navigation'; + +/** + * This class holds a reference to various browser related objects injected by + * set methods. + */ +export class Api { + private readonly performance: Performance; + /** PreformanceObserver constructor function. */ + private readonly PerformanceObserver: typeof PerformanceObserver; + private readonly windowLocation: Location; + // eslint-disable-next-line @typescript-eslint/ban-types + readonly onFirstInputDelay?: Function; + readonly localStorage?: Storage; + readonly document: Document; + readonly navigator: Navigator; + + constructor(readonly window?: Window) { + if (!window) { + throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); + } + this.performance = window.performance; + this.PerformanceObserver = window.PerformanceObserver; + this.windowLocation = window.location; + this.navigator = window.navigator; + this.document = window.document; + if (this.navigator && this.navigator.cookieEnabled) { + // If user blocks cookies on the browser, accessing localStorage will + // throw an exception. + this.localStorage = window.localStorage; + } + if (window.perfMetrics && window.perfMetrics.onFirstInputDelay) { + this.onFirstInputDelay = window.perfMetrics.onFirstInputDelay; + } + } + + getUrl(): string { + // Do not capture the string query part of url. + return this.windowLocation.href.split('?')[0]; + } + + mark(name: string): void { + if (!this.performance || !this.performance.mark) { + return; + } + this.performance.mark(name); + } + + measure(measureName: string, mark1: string, mark2: string): void { + if (!this.performance || !this.performance.measure) { + return; + } + this.performance.measure(measureName, mark1, mark2); + } + + getEntriesByType(type: EntryType): PerformanceEntry[] { + if (!this.performance || !this.performance.getEntriesByType) { + return []; + } + return this.performance.getEntriesByType(type); + } + + getEntriesByName(name: string): PerformanceEntry[] { + if (!this.performance || !this.performance.getEntriesByName) { + return []; + } + return this.performance.getEntriesByName(name); + } + + getTimeOrigin(): number { + // Polyfill the time origin with performance.timing.navigationStart. + return ( + this.performance && + (this.performance.timeOrigin || this.performance.timing.navigationStart) + ); + } + + requiredApisAvailable(): boolean { + if ( + !fetch || + !Promise || + !this.navigator || + !this.navigator.cookieEnabled + ) { + consoleLogger.info( + 'Firebase Performance cannot start if browser does not support fetch and Promise or cookie is disabled.' + ); + return false; + } + + if (!isIndexedDBAvailable()) { + consoleLogger.info('IndexedDB is not supported by current browswer'); + return false; + } + return true; + } + + setupObserver( + entryType: EntryType, + callback: (entry: PerformanceEntry) => void + ): void { + if (!this.PerformanceObserver) { + return; + } + const observer = new this.PerformanceObserver(list => { + for (const entry of list.getEntries()) { + // `entry` is a PerformanceEntry instance. + callback(entry); + } + }); + + // Start observing the entry types you care about. + observer.observe({ entryTypes: [entryType] }); + } + + static getInstance(): Api { + if (apiInstance === undefined) { + apiInstance = new Api(windowInstance); + } + return apiInstance; + } +} + +export function setupApi(window: Window): void { + windowInstance = window; +} diff --git a/packages-exp/performance-exp/src/services/iid_service.test.ts b/packages-exp/performance-exp/src/services/iid_service.test.ts new file mode 100644 index 00000000000..3a768744fa4 --- /dev/null +++ b/packages-exp/performance-exp/src/services/iid_service.test.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub } from 'sinon'; +import { expect } from 'chai'; +import { SettingsService } from './settings_service'; +import { + getIid, + getIidPromise, + getAuthenticationToken, + getAuthTokenPromise +} from './iid_service'; +import '../../test/setup'; +import { FirebaseInstallations } from '@firebase/installations-types'; + +describe('Firebase Perofmrance > iid_service', () => { + const IID = 'fid'; + const AUTH_TOKEN = 'authToken'; + + before(() => { + const getId = stub().resolves(IID); + const getToken = stub().resolves(AUTH_TOKEN); + SettingsService.prototype.installationsService = ({ + getId, + getToken + } as unknown) as FirebaseInstallations; + }); + + describe('getIidPromise', () => { + it('provides iid', async () => { + const iid = await getIidPromise(); + + expect(iid).to.be.equal(IID); + expect(getIid()).to.be.equal(IID); + }); + }); + + describe('getAuthTokenPromise', () => { + it('provides authentication token', async () => { + const token = await getAuthTokenPromise(); + + expect(token).to.be.equal(AUTH_TOKEN); + expect(getAuthenticationToken()).to.be.equal(AUTH_TOKEN); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/services/iid_service.ts b/packages-exp/performance-exp/src/services/iid_service.ts new file mode 100644 index 00000000000..5213900ba47 --- /dev/null +++ b/packages-exp/performance-exp/src/services/iid_service.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { SettingsService } from './settings_service'; + +let iid: string | undefined; +let authToken: string | undefined; + +export function getIidPromise(): Promise { + const iidPromise = SettingsService.getInstance().installationsService.getId(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + iidPromise.then((iidVal: string) => { + iid = iidVal; + }); + return iidPromise; +} + +// This method should be used after the iid is retrieved by getIidPromise method. +export function getIid(): string | undefined { + return iid; +} + +export function getAuthTokenPromise(): Promise { + const authTokenPromise = SettingsService.getInstance().installationsService.getToken(); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + authTokenPromise.then((authTokenVal: string) => { + authToken = authTokenVal; + }); + return authTokenPromise; +} + +export function getAuthenticationToken(): string | undefined { + return authToken; +} diff --git a/packages-exp/performance-exp/src/services/initialization_service.test.ts b/packages-exp/performance-exp/src/services/initialization_service.test.ts new file mode 100644 index 00000000000..832081bdc3c --- /dev/null +++ b/packages-exp/performance-exp/src/services/initialization_service.test.ts @@ -0,0 +1,65 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub } from 'sinon'; +import { expect } from 'chai'; +import { + getInitializationPromise, + isPerfInitialized +} from './initialization_service'; +import { setupApi } from './api_service'; +import { SettingsService } from './settings_service'; +import { FirebaseApp } from '@firebase/app-types'; +import '../../test/setup'; + +describe('Firebase Perofmrance > initialization_service', () => { + const IID = 'fid'; + const AUTH_TOKEN = 'authToken'; + const getId = stub(); + const getToken = stub(); + + const mockWindow = { ...self }; + mockWindow.document = { ...mockWindow.document, readyState: 'complete' }; + + beforeEach(() => { + SettingsService.prototype.firebaseAppInstance = ({ + installations: () => ({ getId, getToken }) + } as unknown) as FirebaseApp; + + stub(self, 'fetch').resolves(new Response('{}')); + mockWindow.localStorage = { ...mockWindow.localStorage, setItem: stub() }; + + setupApi(mockWindow); + }); + + it('changes initialization status after initialization is done', async () => { + getId.resolves(IID); + getToken.resolves(AUTH_TOKEN); + await getInitializationPromise(); + + expect(isPerfInitialized()).to.be.true; + }); + + it('returns initilization as not done before promise is resolved', async () => { + getId.resolves(IID); + getToken.resolves(AUTH_TOKEN); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + getInitializationPromise(); + + expect(isPerfInitialized()).to.be.false; + }); +}); diff --git a/packages-exp/performance-exp/src/services/initialization_service.ts b/packages-exp/performance-exp/src/services/initialization_service.ts new file mode 100644 index 00000000000..96226867122 --- /dev/null +++ b/packages-exp/performance-exp/src/services/initialization_service.ts @@ -0,0 +1,77 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { getIidPromise } from './iid_service'; +import { getConfig } from './remote_config_service'; +import { Api } from './api_service'; + +const enum InitializationStatus { + notInitialized = 1, + initializationPending, + initialized +} + +let initializationStatus = InitializationStatus.notInitialized; + +let initializationPromise: Promise | undefined; + +export function getInitializationPromise(): Promise { + initializationStatus = InitializationStatus.initializationPending; + + initializationPromise = initializationPromise || initializePerf(); + + return initializationPromise; +} + +export function isPerfInitialized(): boolean { + return initializationStatus === InitializationStatus.initialized; +} + +function initializePerf(): Promise { + return getDocumentReadyComplete() + .then(() => getIidPromise()) + .then(iid => getConfig(iid)) + .then( + () => changeInitializationStatus(), + () => changeInitializationStatus() + ); +} + +/** + * Returns a promise which resolves whenever the document readystate is complete or + * immediately if it is called after page load complete. + */ +function getDocumentReadyComplete(): Promise { + const document = Api.getInstance().document; + return new Promise(resolve => { + if (document && document.readyState !== 'complete') { + const handler = (): void => { + if (document.readyState === 'complete') { + document.removeEventListener('readystatechange', handler); + resolve(); + } + }; + document.addEventListener('readystatechange', handler); + } else { + resolve(); + } + }); +} + +function changeInitializationStatus(): void { + initializationStatus = InitializationStatus.initialized; +} diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.test.ts b/packages-exp/performance-exp/src/services/oob_resources_service.test.ts new file mode 100644 index 00000000000..9534d7aa087 --- /dev/null +++ b/packages-exp/performance-exp/src/services/oob_resources_service.test.ts @@ -0,0 +1,199 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { + spy, + stub, + SinonSpy, + SinonStub, + useFakeTimers, + SinonFakeTimers +} from 'sinon'; +import { expect } from 'chai'; +import { Api, setupApi, EntryType } from './api_service'; +import * as iidService from './iid_service'; +import { setupOobResources } from './oob_resources_service'; +import { createNetworkRequestEntry } from '../resources/network_request'; +import { Trace } from '../resources/trace'; +import '../../test/setup'; + +describe('Firebase Performance > oob_resources_service', () => { + const MOCK_ID = 'idasdfsffe'; + + const NAVIGATION_PERFORMANCE_ENTRY: PerformanceNavigationTiming = { + connectEnd: 2.9499998781830072, + connectStart: 2.9499998781830072, + decodedBodySize: 1519, + domComplete: 186.48499995470047, + domContentLoadedEventEnd: 64.0499999281019, + domContentLoadedEventStart: 62.440000008791685, + domInteractive: 62.42000008933246, + domainLookupEnd: 2.9499998781830072, + domainLookupStart: 2.9499998781830072, + duration: 187.7349999267608, + encodedBodySize: 732, + entryType: 'navigation', + fetchStart: 2.9499998781830072, + initiatorType: 'navigation', + loadEventEnd: 187.7349999267608, + loadEventStart: 187.72999988868833, + name: 'https://test.firebase.com/', + nextHopProtocol: 'h2', + redirectCount: 0, + redirectEnd: 0, + redirectStart: 0, + requestStart: 5.034999921917915, + responseEnd: 9.305000072345138, + responseStart: 8.940000087022781, + secureConnectionStart: 0, + startTime: 0, + transferSize: 1259, + type: 'reload', + unloadEventEnd: 14.870000071823597, + unloadEventStart: 14.870000071823597, + workerStart: 0, + toJSON: () => {} + }; + + const PAINT_PERFORMANCE_ENTRY: PerformanceEntry = { + duration: 0, + entryType: 'paint', + name: 'first-contentful-paint', + startTime: 122.18499998562038, + toJSON: () => {} + }; + + let getIidStub: SinonStub<[], string | undefined>; + let apiGetInstanceSpy: SinonSpy<[], Api>; + let getEntriesByTypeStub: SinonStub<[EntryType], PerformanceEntry[]>; + let setupObserverStub: SinonStub< + [EntryType, (entry: PerformanceEntry) => void], + void + >; + let createOobTraceStub: SinonStub< + [PerformanceNavigationTiming[], PerformanceEntry[], (number | undefined)?], + void + >; + let clock: SinonFakeTimers; + + setupApi(self); + + beforeEach(() => { + getIidStub = stub(iidService, 'getIid'); + apiGetInstanceSpy = spy(Api, 'getInstance'); + clock = useFakeTimers(); + getEntriesByTypeStub = stub(Api.prototype, 'getEntriesByType').callsFake( + entry => { + if (entry === 'navigation') { + return [NAVIGATION_PERFORMANCE_ENTRY]; + } + return [PAINT_PERFORMANCE_ENTRY]; + } + ); + setupObserverStub = stub(Api.prototype, 'setupObserver'); + createOobTraceStub = stub(Trace, 'createOobTrace'); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('setupOobResources', () => { + it('does not start if there is no iid', () => { + getIidStub.returns(undefined); + setupOobResources(); + + expect(apiGetInstanceSpy).not.to.be.called; + }); + + it('sets up network request collection', () => { + getIidStub.returns(MOCK_ID); + setupOobResources(); + clock.tick(1); + + expect(apiGetInstanceSpy).to.be.called; + expect(getEntriesByTypeStub).to.be.calledWith('resource'); + expect(setupObserverStub).to.be.calledWithExactly( + 'resource', + createNetworkRequestEntry + ); + }); + + it('sets up page load trace collection', () => { + getIidStub.returns(MOCK_ID); + setupOobResources(); + clock.tick(1); + + expect(apiGetInstanceSpy).to.be.called; + expect(getEntriesByTypeStub).to.be.calledWith('navigation'); + expect(getEntriesByTypeStub).to.be.calledWith('paint'); + expect(createOobTraceStub).to.be.calledWithExactly( + [NAVIGATION_PERFORMANCE_ENTRY], + [PAINT_PERFORMANCE_ENTRY] + ); + }); + + it('waits for first input delay if polyfill is available', () => { + getIidStub.returns(MOCK_ID); + const api = Api.getInstance(); + //@ts-ignore Assignment to read-only property. + api.onFirstInputDelay = stub(); + setupOobResources(); + clock.tick(1); + + expect(api.onFirstInputDelay).to.be.called; + expect(createOobTraceStub).not.to.be.called; + clock.tick(5000); + expect(createOobTraceStub).to.be.calledWithExactly( + [NAVIGATION_PERFORMANCE_ENTRY], + [PAINT_PERFORMANCE_ENTRY] + ); + }); + + it('logs first input delay if polyfill is available and callback is called', () => { + getIidStub.returns(MOCK_ID); + const api = Api.getInstance(); + const FIRST_INPUT_DELAY = 123; + // Underscore is to avoid compiler comlaining about variable being declared but not used. + type FirstInputDelayCallback = (firstInputDelay: number) => void; + let firstInputDelayCallback: FirstInputDelayCallback = (): void => {}; + //@ts-ignore Assignment to read-only property. + api.onFirstInputDelay = (cb: FirstInputDelayCallback) => { + firstInputDelayCallback = cb; + }; + setupOobResources(); + clock.tick(1); + firstInputDelayCallback(FIRST_INPUT_DELAY); + + expect(createOobTraceStub).to.be.calledWithExactly( + [NAVIGATION_PERFORMANCE_ENTRY], + [PAINT_PERFORMANCE_ENTRY], + FIRST_INPUT_DELAY + ); + }); + + it('sets up user timing traces', () => { + getIidStub.returns(MOCK_ID); + setupOobResources(); + clock.tick(1); + + expect(apiGetInstanceSpy).to.be.called; + expect(getEntriesByTypeStub).to.be.calledWith('measure'); + expect(setupObserverStub).to.be.calledWith('measure'); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.ts b/packages-exp/performance-exp/src/services/oob_resources_service.ts new file mode 100644 index 00000000000..0e01236903e --- /dev/null +++ b/packages-exp/performance-exp/src/services/oob_resources_service.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { Api } from './api_service'; +import { Trace } from '../resources/trace'; +import { createNetworkRequestEntry } from '../resources/network_request'; +import { TRACE_MEASURE_PREFIX } from '../constants'; +import { getIid } from './iid_service'; + +const FID_WAIT_TIME_MS = 5000; + +export function setupOobResources(): void { + // Do not initialize unless iid is available. + if (!getIid()) { + return; + } + // The load event might not have fired yet, and that means performance navigation timing + // object has a duration of 0. The setup should run after all current tasks in js queue. + setTimeout(() => setupOobTraces(), 0); + setTimeout(() => setupNetworkRequests(), 0); + setTimeout(() => setupUserTimingTraces(), 0); +} + +function setupNetworkRequests(): void { + const api = Api.getInstance(); + const resources = api.getEntriesByType('resource'); + for (const resource of resources) { + createNetworkRequestEntry(resource); + } + api.setupObserver('resource', createNetworkRequestEntry); +} + +function setupOobTraces(): void { + const api = Api.getInstance(); + const navigationTimings = api.getEntriesByType( + 'navigation' + ) as PerformanceNavigationTiming[]; + const paintTimings = api.getEntriesByType('paint'); + // If First Input Desly polyfill is added to the page, report the fid value. + // https://github.com/GoogleChromeLabs/first-input-delay + if (api.onFirstInputDelay) { + // If the fid call back is not called for certain time, continue without it. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let timeoutId: any = setTimeout(() => { + Trace.createOobTrace(navigationTimings, paintTimings); + timeoutId = undefined; + }, FID_WAIT_TIME_MS); + api.onFirstInputDelay((fid: number) => { + if (timeoutId) { + clearTimeout(timeoutId); + Trace.createOobTrace(navigationTimings, paintTimings, fid); + } + }); + } else { + Trace.createOobTrace(navigationTimings, paintTimings); + } +} + +function setupUserTimingTraces(): void { + const api = Api.getInstance(); + // Run through the measure performance entries collected up to this point. + const measures = api.getEntriesByType('measure'); + for (const measure of measures) { + createUserTimingTrace(measure); + } + // Setup an observer to capture the measures from this point on. + api.setupObserver('measure', createUserTimingTrace); +} + +function createUserTimingTrace(measure: PerformanceEntry): void { + const measureName = measure.name; + // Do not create a trace, if the user timing marks and measures are created by the sdk itself. + if ( + measureName.substring(0, TRACE_MEASURE_PREFIX.length) === + TRACE_MEASURE_PREFIX + ) { + return; + } + Trace.createUserTimingTrace(measureName); +} diff --git a/packages-exp/performance-exp/src/services/perf_logger.test.ts b/packages-exp/performance-exp/src/services/perf_logger.test.ts new file mode 100644 index 00000000000..76148eeca0b --- /dev/null +++ b/packages-exp/performance-exp/src/services/perf_logger.test.ts @@ -0,0 +1,383 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub, SinonStub, useFakeTimers, SinonFakeTimers } from 'sinon'; +import { Trace } from '../resources/trace'; +import * as transportService from './transport_service'; +import * as iidService from './iid_service'; +import { expect } from 'chai'; +import { Api, setupApi } from './api_service'; +import { SettingsService } from './settings_service'; +import { FirebaseApp } from '@firebase/app-types'; +import * as initializationService from './initialization_service'; +import { SDK_VERSION } from '../constants'; +import * as attributeUtils from '../utils/attributes_utils'; +import { createNetworkRequestEntry } from '../resources/network_request'; +import '../../test/setup'; +import { mergeStrings } from '../utils/string_merger'; + +describe('Performance Monitoring > perf_logger', () => { + const IID = 'idasdfsffe'; + const PAGE_URL = 'http://mock-page.com'; + const APP_ID = '1:123:web:2er'; + const VISIBILITY_STATE = 3; + const EFFECTIVE_CONNECTION_TYPE = 2; + const SERVICE_WORKER_STATUS = 3; + const TIME_ORIGIN = 1556512199893.9033; + const TRACE_NAME = 'testTrace'; + const START_TIME = 12345; + const DURATION = 321; + // Perf event header which is constant across tests in this file. + const WEBAPP_INFO = `"application_info":{"google_app_id":"${APP_ID}",\ +"app_instance_id":"${IID}","web_app_info":{"sdk_version":"${SDK_VERSION}",\ +"page_url":"${PAGE_URL}","service_worker_status":${SERVICE_WORKER_STATUS},\ +"visibility_state":${VISIBILITY_STATE},"effective_connection_type":${EFFECTIVE_CONNECTION_TYPE}},\ +"application_process_state":0}`; + + let addToQueueStub: SinonStub< + Array<{ message: string; eventTime: number }>, + void + >; + let getIidStub: SinonStub<[], string | undefined>; + let clock: SinonFakeTimers; + + function mockTransportHandler( + serializer: (...args: any[]) => string + ): (...args: any[]) => void { + return (...args) => { + const message = serializer(...args); + addToQueueStub({ + message, + eventTime: Date.now() + }); + }; + } + + setupApi(self); + + beforeEach(() => { + getIidStub = stub(iidService, 'getIid'); + addToQueueStub = stub(); + stub(transportService, 'transportHandler').callsFake(mockTransportHandler); + stub(Api.prototype, 'getUrl').returns(PAGE_URL); + stub(Api.prototype, 'getTimeOrigin').returns(TIME_ORIGIN); + stub(initializationService, 'isPerfInitialized').returns(true); + stub(attributeUtils, 'getEffectiveConnectionType').returns( + EFFECTIVE_CONNECTION_TYPE + ); + stub(attributeUtils, 'getServiceWorkerStatus').returns( + SERVICE_WORKER_STATUS + ); + SettingsService.prototype.firebaseAppInstance = ({ + options: { appId: APP_ID } + } as unknown) as FirebaseApp; + clock = useFakeTimers(); + }); + + describe('logTrace', () => { + it('creates, serializes and sends a trace to transport service', () => { + const EXPECTED_TRACE_MESSAGE = + `{` + + WEBAPP_INFO + + `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ +"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ +"counters":{"counter1":3},"custom_attributes":{"attr":"val"}}}`; + getIidStub.returns(IID); + stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logTraceAfterSampling = true; + const trace = new Trace(TRACE_NAME); + trace.putAttribute('attr', 'val'); + trace.putMetric('counter1', 3); + trace.record(START_TIME, DURATION); + clock.tick(1); + + expect(addToQueueStub).to.be.called; + expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( + EXPECTED_TRACE_MESSAGE + ); + }); + + it('does not log an event if cookies are disabled in the browser', () => { + stub(Api.prototype, 'requiredApisAvailable').returns(false); + stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); + const trace = new Trace(TRACE_NAME); + trace.record(START_TIME, DURATION); + clock.tick(1); + + expect(addToQueueStub).not.to.be.called; + }); + + it('ascertains that the max number of customMetric allowed is 32', () => { + const EXPECTED_TRACE_MESSAGE = + `{` + + WEBAPP_INFO + + `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ +"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ +"counters":{"counter1":1,"counter2":2,"counter3":3,"counter4":4,"counter5":5,"counter6":6,\ +"counter7":7,"counter8":8,"counter9":9,"counter10":10,"counter11":11,"counter12":12,\ +"counter13":13,"counter14":14,"counter15":15,"counter16":16,"counter17":17,"counter18":18,\ +"counter19":19,"counter20":20,"counter21":21,"counter22":22,"counter23":23,"counter24":24,\ +"counter25":25,"counter26":26,"counter27":27,"counter28":28,"counter29":29,"counter30":30,\ +"counter31":31,"counter32":32}}}`; + getIidStub.returns(IID); + stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logTraceAfterSampling = true; + const trace = new Trace(TRACE_NAME); + for (let i = 1; i <= 32; i++) { + trace.putMetric('counter' + i, i); + } + trace.record(START_TIME, DURATION); + clock.tick(1); + + expect(addToQueueStub).to.be.called; + expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( + EXPECTED_TRACE_MESSAGE + ); + }); + + it('ascertains that the max number of custom attributes allowed is 5', () => { + const EXPECTED_TRACE_MESSAGE = + `{` + + WEBAPP_INFO + + `,"trace_metric":{"name":"${TRACE_NAME}","is_auto":false,\ +"client_start_time_us":${START_TIME * 1000},"duration_us":${DURATION * 1000},\ +"custom_attributes":{"attr1":"val1","attr2":"val2","attr3":"val3","attr4":"val4","attr5":"val5"}}}`; + getIidStub.returns(IID); + stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logTraceAfterSampling = true; + const trace = new Trace(TRACE_NAME); + for (let i = 1; i <= 5; i++) { + trace.putAttribute('attr' + i, 'val' + i); + } + trace.record(START_TIME, DURATION); + clock.tick(1); + + expect(addToQueueStub).to.be.called; + expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( + EXPECTED_TRACE_MESSAGE + ); + }); + }); + + describe('logPageLoadTrace', () => { + it('creates, serializes and sends a page load trace to cc service', () => { + const flooredStartTime = Math.floor(TIME_ORIGIN * 1000); + const EXPECTED_TRACE_MESSAGE = `{"application_info":{"google_app_id":"${APP_ID}",\ +"app_instance_id":"${IID}","web_app_info":{"sdk_version":"${SDK_VERSION}",\ +"page_url":"${PAGE_URL}","service_worker_status":${SERVICE_WORKER_STATUS},\ +"visibility_state":${ + attributeUtils.VisibilityState.VISIBLE + },"effective_connection_type":${EFFECTIVE_CONNECTION_TYPE}},\ +"application_process_state":0},"trace_metric":{"name":"_wt_${PAGE_URL}","is_auto":true,\ +"client_start_time_us":${flooredStartTime},"duration_us":${DURATION * 1000},\ +"counters":{"domInteractive":10000,"domContentLoadedEventEnd":20000,"loadEventEnd":10000,\ +"_fp":40000,"_fcp":50000,"_fid":90000}}}`; + getIidStub.returns(IID); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logTraceAfterSampling = true; + + stub(attributeUtils, 'getVisibilityState').returns( + attributeUtils.VisibilityState.VISIBLE + ); + + const navigationTiming: PerformanceNavigationTiming = { + domComplete: 100, + domContentLoadedEventEnd: 20, + domContentLoadedEventStart: 10, + domInteractive: 10, + loadEventEnd: 10, + loadEventStart: 10, + redirectCount: 10, + type: 'navigate', + unloadEventEnd: 10, + unloadEventStart: 10, + duration: DURATION + } as PerformanceNavigationTiming; + + const navigationTimings: PerformanceNavigationTiming[] = [ + navigationTiming + ]; + + const firstPaint: PerformanceEntry = { + name: 'first-paint', + startTime: 40, + duration: 100, + entryType: 'url', + toJSON() {} + }; + + const firstContentfulPaint: PerformanceEntry = { + name: 'first-contentful-paint', + startTime: 50, + duration: 100, + entryType: 'url', + toJSON() {} + }; + + const paintTimings: PerformanceEntry[] = [ + firstPaint, + firstContentfulPaint + ]; + + Trace.createOobTrace(navigationTimings, paintTimings, 90); + clock.tick(1); + + expect(addToQueueStub).to.be.called; + expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( + EXPECTED_TRACE_MESSAGE + ); + }); + }); + + describe('logNetworkRequest', () => { + it('creates, serializes and sends a network request to transport service', () => { + const RESOURCE_PERFORMANCE_ENTRY: PerformanceResourceTiming = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 39.610000094398856, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 5645.689999917522, + initiatorType: 'fetch', + name: 'https://test.com/abc', + nextHopProtocol: 'http/2+quic/43', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 5685.300000011921, + responseStart: 0, + secureConnectionStart: 0, + startTime: 5645.689999917522, + transferSize: 0, + workerStart: 0, + toJSON: () => {} + }; + const START_TIME = Math.floor( + (TIME_ORIGIN + RESOURCE_PERFORMANCE_ENTRY.startTime) * 1000 + ); + const TIME_TO_RESPONSE_COMPLETED = Math.floor( + (RESOURCE_PERFORMANCE_ENTRY.responseEnd - + RESOURCE_PERFORMANCE_ENTRY.startTime) * + 1000 + ); + const EXPECTED_NETWORK_MESSAGE = + `{` + + WEBAPP_INFO + + `,\ +"network_request_metric":{"url":"${RESOURCE_PERFORMANCE_ENTRY.name}",\ +"http_method":0,"http_response_code":200,\ +"response_payload_bytes":${RESOURCE_PERFORMANCE_ENTRY.transferSize},\ +"client_start_time_us":${START_TIME},\ +"time_to_response_completed_us":${TIME_TO_RESPONSE_COMPLETED}}}`; + getIidStub.returns(IID); + stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logNetworkAfterSampling = true; + // Calls logNetworkRequest under the hood. + createNetworkRequestEntry(RESOURCE_PERFORMANCE_ENTRY); + clock.tick(1); + + expect(addToQueueStub).to.be.called; + expect(addToQueueStub.getCall(0).args[0].message).to.be.equal( + EXPECTED_NETWORK_MESSAGE + ); + }); + + // Performance SDK doesn't instrument requests sent from SDK itself, therefore blacklist + // requests sent to cc endpoint. + it('skips performance collection if domain is cc', () => { + const CC_NETWORK_PERFORMANCE_ENTRY: PerformanceResourceTiming = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 39.610000094398856, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 5645.689999917522, + initiatorType: 'fetch', + name: 'https://firebaselogging.googleapis.com/v0cc/log?message=a', + nextHopProtocol: 'http/2+quic/43', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 5685.300000011921, + responseStart: 0, + secureConnectionStart: 0, + startTime: 5645.689999917522, + transferSize: 0, + workerStart: 0, + toJSON: () => {} + }; + getIidStub.returns(IID); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logNetworkAfterSampling = true; + // Calls logNetworkRequest under the hood. + createNetworkRequestEntry(CC_NETWORK_PERFORMANCE_ENTRY); + clock.tick(1); + + expect(addToQueueStub).not.called; + }); + + // Performance SDK doesn't instrument requests sent from SDK itself, therefore blacklist + // requests sent to fl endpoint. + it('skips performance collection if domain is fl', () => { + const FL_NETWORK_PERFORMANCE_ENTRY: PerformanceResourceTiming = { + connectEnd: 0, + connectStart: 0, + decodedBodySize: 0, + domainLookupEnd: 0, + domainLookupStart: 0, + duration: 39.610000094398856, + encodedBodySize: 0, + entryType: 'resource', + fetchStart: 5645.689999917522, + initiatorType: 'fetch', + name: mergeStrings( + 'hts/frbslgigp.ogepscmv/ieo/eaylg', + 'tp:/ieaeogn-agolai.o/1frlglgc/o' + ), + nextHopProtocol: 'http/2+quic/43', + redirectEnd: 0, + redirectStart: 0, + requestStart: 0, + responseEnd: 5685.300000011921, + responseStart: 0, + secureConnectionStart: 0, + startTime: 5645.689999917522, + transferSize: 0, + workerStart: 0, + toJSON: () => {} + }; + getIidStub.returns(IID); + SettingsService.getInstance().loggingEnabled = true; + SettingsService.getInstance().logNetworkAfterSampling = true; + // Calls logNetworkRequest under the hood. + createNetworkRequestEntry(FL_NETWORK_PERFORMANCE_ENTRY); + clock.tick(1); + + expect(addToQueueStub).not.called; + }); + }); +}); diff --git a/packages-exp/performance-exp/src/services/perf_logger.ts b/packages-exp/performance-exp/src/services/perf_logger.ts new file mode 100644 index 00000000000..2d2daa22fca --- /dev/null +++ b/packages-exp/performance-exp/src/services/perf_logger.ts @@ -0,0 +1,243 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { getIid } from './iid_service'; +import { NetworkRequest } from '../resources/network_request'; +import { Trace } from '../resources/trace'; +import { Api } from './api_service'; +import { SettingsService } from './settings_service'; +import { + getServiceWorkerStatus, + getVisibilityState, + VisibilityState, + getEffectiveConnectionType +} from '../utils/attributes_utils'; +import { + isPerfInitialized, + getInitializationPromise +} from './initialization_service'; +import { transportHandler } from './transport_service'; +import { SDK_VERSION } from '../constants'; + +const enum ResourceType { + NetworkRequest, + Trace +} + +/* eslint-disable camelcase */ +interface ApplicationInfo { + google_app_id: string; + app_instance_id?: string; + web_app_info: WebAppInfo; + application_process_state: number; +} + +interface WebAppInfo { + sdk_version: string; + page_url: string; + service_worker_status: number; + visibility_state: number; + effective_connection_type: number; +} + +interface PerfNetworkLog { + application_info: ApplicationInfo; + network_request_metric: NetworkRequestMetric; +} + +interface PerfTraceLog { + application_info: ApplicationInfo; + trace_metric: TraceMetric; +} + +interface NetworkRequestMetric { + url: string; + http_method: number; + http_response_code: number; + response_payload_bytes?: number; + client_start_time_us?: number; + time_to_response_initiated_us?: number; + time_to_response_completed_us?: number; +} + +interface TraceMetric { + name: string; + is_auto: boolean; + client_start_time_us: number; + duration_us: number; + counters?: { [key: string]: number }; + custom_attributes?: { [key: string]: string }; +} + +/* eslint-enble camelcase */ + +let logger: ( + resource: NetworkRequest | Trace, + resourceType: ResourceType +) => void | undefined; +// This method is not called before initialization. +function sendLog( + resource: NetworkRequest | Trace, + resourceType: ResourceType +): void { + if (!logger) { + logger = transportHandler(serializer); + } + logger(resource, resourceType); +} + +export function logTrace(trace: Trace): void { + const settingsService = SettingsService.getInstance(); + // Do not log if trace is auto generated and instrumentation is disabled. + if (!settingsService.instrumentationEnabled && trace.isAuto) { + return; + } + // Do not log if trace is custom and data collection is disabled. + if (!settingsService.dataCollectionEnabled && !trace.isAuto) { + return; + } + // Do not log if required apis are not available. + if (!Api.getInstance().requiredApisAvailable()) { + return; + } + + // Only log the page load auto traces if page is visible. + if (trace.isAuto && getVisibilityState() !== VisibilityState.VISIBLE) { + return; + } + + if ( + !settingsService.loggingEnabled || + !settingsService.logTraceAfterSampling + ) { + return; + } + + if (isPerfInitialized()) { + sendTraceLog(trace); + } else { + // Custom traces can be used before the initialization but logging + // should wait until after. + getInitializationPromise().then( + () => sendTraceLog(trace), + () => sendTraceLog(trace) + ); + } +} + +function sendTraceLog(trace: Trace): void { + if (getIid()) { + setTimeout(() => sendLog(trace, ResourceType.Trace), 0); + } +} + +export function logNetworkRequest(networkRequest: NetworkRequest): void { + const settingsService = SettingsService.getInstance(); + // Do not log network requests if instrumentation is disabled. + if (!settingsService.instrumentationEnabled) { + return; + } + + // Do not log the js sdk's call to transport service domain to avoid unnecessary cycle. + // Need to blacklist both old and new endpoints to avoid migration gap. + const networkRequestUrl = networkRequest.url; + + // Blacklist old log endpoint and new transport endpoint. + // Because Performance SDK doesn't instrument requests sent from SDK itself. + const logEndpointUrl = settingsService.logEndPointUrl.split('?')[0]; + const flEndpointUrl = settingsService.flTransportEndpointUrl.split('?')[0]; + if ( + networkRequestUrl === logEndpointUrl || + networkRequestUrl === flEndpointUrl + ) { + return; + } + + if ( + !settingsService.loggingEnabled || + !settingsService.logNetworkAfterSampling + ) { + return; + } + + setTimeout(() => sendLog(networkRequest, ResourceType.NetworkRequest), 0); +} + +function serializer( + resource: NetworkRequest | Trace, + resourceType: ResourceType +): string { + if (resourceType === ResourceType.NetworkRequest) { + return serializeNetworkRequest(resource as NetworkRequest); + } + return serializeTrace(resource as Trace); +} + +function serializeNetworkRequest(networkRequest: NetworkRequest): string { + const networkRequestMetric: NetworkRequestMetric = { + url: networkRequest.url, + http_method: networkRequest.httpMethod || 0, + http_response_code: 200, + response_payload_bytes: networkRequest.responsePayloadBytes, + client_start_time_us: networkRequest.startTimeUs, + time_to_response_initiated_us: networkRequest.timeToResponseInitiatedUs, + time_to_response_completed_us: networkRequest.timeToResponseCompletedUs + }; + const perfMetric: PerfNetworkLog = { + application_info: getApplicationInfo(), + network_request_metric: networkRequestMetric + }; + return JSON.stringify(perfMetric); +} + +function serializeTrace(trace: Trace): string { + const traceMetric: TraceMetric = { + name: trace.name, + is_auto: trace.isAuto, + client_start_time_us: trace.startTimeUs, + duration_us: trace.durationUs + }; + + if (Object.keys(trace.counters).length !== 0) { + traceMetric.counters = trace.counters; + } + const customAttributes = trace.getAttributes(); + if (Object.keys(customAttributes).length !== 0) { + traceMetric.custom_attributes = customAttributes; + } + + const perfMetric: PerfTraceLog = { + application_info: getApplicationInfo(), + trace_metric: traceMetric + }; + return JSON.stringify(perfMetric); +} + +function getApplicationInfo(): ApplicationInfo { + return { + google_app_id: SettingsService.getInstance().getAppId(), + app_instance_id: getIid(), + web_app_info: { + sdk_version: SDK_VERSION, + page_url: Api.getInstance().getUrl(), + service_worker_status: getServiceWorkerStatus(), + visibility_state: getVisibilityState(), + effective_connection_type: getEffectiveConnectionType() + }, + application_process_state: 0 + }; +} diff --git a/packages-exp/performance-exp/src/services/remote_config_service.test.ts b/packages-exp/performance-exp/src/services/remote_config_service.test.ts new file mode 100644 index 00000000000..7bbcf3722d5 --- /dev/null +++ b/packages-exp/performance-exp/src/services/remote_config_service.test.ts @@ -0,0 +1,274 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub, useFakeTimers, SinonFakeTimers, SinonStub } from 'sinon'; +import { expect } from 'chai'; +import { SettingsService } from './settings_service'; +import { CONFIG_EXPIRY_LOCAL_STORAGE_KEY } from '../constants'; +import { setupApi, Api } from './api_service'; +import * as iidService from './iid_service'; +import { getConfig } from './remote_config_service'; +import { FirebaseApp } from '@firebase/app-types'; +import '../../test/setup'; + +describe('Performance Monitoring > remote_config_service', () => { + const IID = 'asd123'; + const AUTH_TOKEN = 'auth_token'; + const LOG_URL = 'https://firebaselogging.test.com'; + const TRANSPORT_KEY = 'pseudo-transport-key'; + const LOG_SOURCE = 2; + const NETWORK_SAMPLIG_RATE = 0.25; + const TRACE_SAMPLING_RATE = 0.5; + const GLOBAL_CLOCK_NOW = 1556524895326; + const STRINGIFIED_CONFIG = `{"entries":{"fpr_enabled":"true",\ + "fpr_log_endpoint_url":"https://firebaselogging.test.com",\ + "fpr_log_transport_key":"pseudo-transport-key",\ + "fpr_log_source":"2","fpr_vc_network_request_sampling_rate":"0.250000",\ + "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ + "state":"UPDATE"}`; + const PROJECT_ID = 'project1'; + const APP_ID = '1:23r:web:fewq'; + const API_KEY = 'asdfghjk'; + const NOT_VALID_CONFIG = 'not a valid config and should not be used'; + + let clock: SinonFakeTimers; + + setupApi(self); + const ApiInstance = Api.getInstance(); + + function storageGetItemFakeFactory( + expiry: string, + config: string + ): (key: string) => string { + return (key: string) => { + if (key === CONFIG_EXPIRY_LOCAL_STORAGE_KEY) { + return expiry; + } + return config; + }; + } + + function resetSettingsService(): void { + const settingsService = SettingsService.getInstance(); + settingsService.logSource = 462; + settingsService.loggingEnabled = false; + settingsService.networkRequestsSamplingRate = 1; + settingsService.tracesSamplingRate = 1; + } + + // parameterized beforeEach. Should be called at beginning of each test. + function setup( + storageConfig: { expiry: string; config: string }, + fetchConfig?: { reject: boolean; value?: Response } + ): { + storageGetItemStub: SinonStub<[string], string | null>; + fetchStub: SinonStub<[RequestInfo, RequestInit?], Promise>; + } { + const fetchStub = stub(self, 'fetch'); + + if (fetchConfig) { + fetchConfig.reject + ? fetchStub.rejects() + : fetchStub.resolves(fetchConfig.value); + } + + stub(iidService, 'getAuthTokenPromise').returns( + Promise.resolve(AUTH_TOKEN) + ); + + clock = useFakeTimers(GLOBAL_CLOCK_NOW); + SettingsService.prototype.firebaseAppInstance = ({ + options: { projectId: PROJECT_ID, appId: APP_ID, apiKey: API_KEY } + } as unknown) as FirebaseApp; + + // we need to stub the entire localStorage, because storage can't be stubbed in Firefox and IE. + // stubbing on self(window) seems to only work the first time (at least in Firefox), the subsequent + // tests will have the same stub. stub.reset() in afterEach doesn't help either. As a result, we stub on ApiInstance. + // https://github.com/sinonjs/sinon/issues/662 + const storageStub = stub(ApiInstance, 'localStorage'); + const getItemStub: SinonStub<[string], string | null> = stub(); + + storageStub.value({ + getItem: getItemStub.callsFake( + storageGetItemFakeFactory(storageConfig.expiry, storageConfig.config) + ), + setItem: () => {} + }); + + return { storageGetItemStub: getItemStub, fetchStub }; + } + + afterEach(() => { + resetSettingsService(); + clock.restore(); + }); + + describe('getConfig', () => { + it('gets the config from the local storage if available and valid', async () => { + // After global clock. Config not expired. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895330'; + const { storageGetItemStub: getItemStub } = setup({ + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: STRINGIFIED_CONFIG + }); + + await getConfig(IID); + + expect(getItemStub).to.be.called; + expect(SettingsService.getInstance().loggingEnabled).to.be.true; + expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); + expect(SettingsService.getInstance().transportKey).to.equal( + TRANSPORT_KEY + ); + expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); + expect( + SettingsService.getInstance().networkRequestsSamplingRate + ).to.equal(NETWORK_SAMPLIG_RATE); + expect(SettingsService.getInstance().tracesSamplingRate).to.equal( + TRACE_SAMPLING_RATE + ); + }); + + it('does not call remote config if a valid config is in local storage', async () => { + // After global clock. Config not expired. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895330'; + + const { fetchStub } = setup({ + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: STRINGIFIED_CONFIG + }); + + await getConfig(IID); + + expect(fetchStub).not.to.be.called; + }); + + it('gets the config from RC if local version is not valid', async () => { + // Expired local config. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; + + const { storageGetItemStub: getItemStub } = setup( + { expiry: EXPIRY_LOCAL_STORAGE_VALUE, config: STRINGIFIED_CONFIG }, + { reject: false, value: new Response(STRINGIFIED_CONFIG) } + ); + + await getConfig(IID); + + expect(getItemStub).to.be.calledOnce; + expect(SettingsService.getInstance().loggingEnabled).to.be.true; + expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); + expect(SettingsService.getInstance().transportKey).to.equal( + TRANSPORT_KEY + ); + expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); + expect( + SettingsService.getInstance().networkRequestsSamplingRate + ).to.equal(NETWORK_SAMPLIG_RATE); + expect(SettingsService.getInstance().tracesSamplingRate).to.equal( + TRACE_SAMPLING_RATE + ); + }); + + it('does not change the default config if call to RC fails', async () => { + // Expired local config. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; + + setup( + { + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: NOT_VALID_CONFIG + }, + { reject: true } + ); + + await getConfig(IID); + + expect(SettingsService.getInstance().loggingEnabled).to.equal(false); + }); + + it('uses secondary configs if the response does not have all the fields', async () => { + // Expired local config. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; + const STRINGIFIED_PARTIAL_CONFIG = `{"entries":{\ + "fpr_vc_network_request_sampling_rate":"0.250000",\ + "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ + "state":"UPDATE"}`; + + setup( + { + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: NOT_VALID_CONFIG + }, + { reject: false, value: new Response(STRINGIFIED_PARTIAL_CONFIG) } + ); + + await getConfig(IID); + + expect(SettingsService.getInstance().loggingEnabled).to.be.true; + }); + + it('uses secondary configs if the response does not have any fields', async () => { + // Expired local config. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; + const STRINGIFIED_PARTIAL_CONFIG = '{"state":"NO_TEMPLATE"}'; + + setup( + { + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: NOT_VALID_CONFIG + }, + { reject: false, value: new Response(STRINGIFIED_PARTIAL_CONFIG) } + ); + await getConfig(IID); + + expect(SettingsService.getInstance().loggingEnabled).to.be.true; + }); + + it('gets the config from RC even with deprecated transport flag', async () => { + // Expired local config. + const EXPIRY_LOCAL_STORAGE_VALUE = '1556524895320'; + const STRINGIFIED_CUSTOM_CONFIG = `{"entries":{\ + "fpr_vc_network_request_sampling_rate":"0.250000",\ + "fpr_log_transport_web_percent":"100.0",\ + "fpr_vc_session_sampling_rate":"0.250000","fpr_vc_trace_sampling_rate":"0.500000"},\ + "state":"UPDATE"}`; + + const { storageGetItemStub: getItemStub } = setup( + { + expiry: EXPIRY_LOCAL_STORAGE_VALUE, + config: STRINGIFIED_CUSTOM_CONFIG + }, + { reject: false, value: new Response(STRINGIFIED_CONFIG) } + ); + + await getConfig(IID); + + expect(getItemStub).to.be.calledOnce; + expect(SettingsService.getInstance().loggingEnabled).to.be.true; + expect(SettingsService.getInstance().logEndPointUrl).to.equal(LOG_URL); + expect(SettingsService.getInstance().transportKey).to.equal( + TRANSPORT_KEY + ); + expect(SettingsService.getInstance().logSource).to.equal(LOG_SOURCE); + expect( + SettingsService.getInstance().networkRequestsSamplingRate + ).to.equal(NETWORK_SAMPLIG_RATE); + expect(SettingsService.getInstance().tracesSamplingRate).to.equal( + TRACE_SAMPLING_RATE + ); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/services/remote_config_service.ts b/packages-exp/performance-exp/src/services/remote_config_service.ts new file mode 100644 index 00000000000..71f5266c805 --- /dev/null +++ b/packages-exp/performance-exp/src/services/remote_config_service.ts @@ -0,0 +1,233 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { + CONFIG_EXPIRY_LOCAL_STORAGE_KEY, + CONFIG_LOCAL_STORAGE_KEY, + SDK_VERSION +} from '../constants'; +import { consoleLogger } from '../utils/console_logger'; +import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; + +import { Api } from './api_service'; +import { getAuthTokenPromise } from './iid_service'; +import { SettingsService } from './settings_service'; + +const REMOTE_CONFIG_SDK_VERSION = '0.0.1'; + +interface SecondaryConfig { + loggingEnabled?: boolean; + logSource?: number; + logEndPointUrl?: string; + transportKey?: string; + tracesSamplingRate?: number; + networkRequestsSamplingRate?: number; +} + +// These values will be used if the remote config object is successfully +// retrieved, but the template does not have these fields. +const DEFAULT_CONFIGS: SecondaryConfig = { + loggingEnabled: true +}; + +/* eslint-disable camelcase */ +interface RemoteConfigTemplate { + fpr_enabled?: string; + fpr_log_source?: string; + fpr_log_endpoint_url?: string; + fpr_log_transport_key?: string; + fpr_log_transport_web_percent?: string; + fpr_vc_network_request_sampling_rate?: string; + fpr_vc_trace_sampling_rate?: string; + fpr_vc_session_sampling_rate?: string; +} +/* eslint-enable camelcase */ + +interface RemoteConfigResponse { + entries?: RemoteConfigTemplate; + state?: string; +} + +const FIS_AUTH_PREFIX = 'FIREBASE_INSTALLATIONS_AUTH'; + +export function getConfig(iid: string): Promise { + const config = getStoredConfig(); + if (config) { + processConfig(config); + return Promise.resolve(); + } + + return getRemoteConfig(iid) + .then(processConfig) + .then( + config => storeConfig(config), + /** Do nothing for error, use defaults set in settings service. */ + () => {} + ); +} + +function getStoredConfig(): RemoteConfigResponse | undefined { + const localStorage = Api.getInstance().localStorage; + if (!localStorage) { + return; + } + const expiryString = localStorage.getItem(CONFIG_EXPIRY_LOCAL_STORAGE_KEY); + if (!expiryString || !configValid(expiryString)) { + return; + } + + const configStringified = localStorage.getItem(CONFIG_LOCAL_STORAGE_KEY); + if (!configStringified) { + return; + } + try { + const configResponse: RemoteConfigResponse = JSON.parse(configStringified); + return configResponse; + } catch { + return; + } +} + +function storeConfig(config: RemoteConfigResponse | undefined): void { + const localStorage = Api.getInstance().localStorage; + if (!config || !localStorage) { + return; + } + + localStorage.setItem(CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config)); + localStorage.setItem( + CONFIG_EXPIRY_LOCAL_STORAGE_KEY, + String( + Date.now() + + SettingsService.getInstance().configTimeToLive * 60 * 60 * 1000 + ) + ); +} + +const COULD_NOT_GET_CONFIG_MSG = + 'Could not fetch config, will use default configs'; + +function getRemoteConfig( + iid: string +): Promise { + // Perf needs auth token only to retrieve remote config. + return getAuthTokenPromise() + .then(authToken => { + const projectId = SettingsService.getInstance().getProjectId(); + const configEndPoint = `https://firebaseremoteconfig.googleapis.com/v1/projects/${projectId}/namespaces/fireperf:fetch?key=${SettingsService.getInstance().getApiKey()}`; + const request = new Request(configEndPoint, { + method: 'POST', + headers: { Authorization: `${FIS_AUTH_PREFIX} ${authToken}` }, + /* eslint-disable camelcase */ + body: JSON.stringify({ + app_instance_id: iid, + app_instance_id_token: authToken, + app_id: SettingsService.getInstance().getAppId(), + app_version: SDK_VERSION, + sdk_version: REMOTE_CONFIG_SDK_VERSION + }) + /* eslint-enable camelcase */ + }); + return fetch(request).then(response => { + if (response.ok) { + return response.json() as RemoteConfigResponse; + } + // In case response is not ok. This will be caught by catch. + throw ERROR_FACTORY.create(ErrorCode.RC_NOT_OK); + }); + }) + .catch(() => { + consoleLogger.info(COULD_NOT_GET_CONFIG_MSG); + return undefined; + }); +} + +/** + * Processes config coming either from calling RC or from local storage. + * This method only runs if call is successful or config in storage + * is valid. + */ +function processConfig( + config?: RemoteConfigResponse +): RemoteConfigResponse | undefined { + if (!config) { + return config; + } + const settingsServiceInstance = SettingsService.getInstance(); + const entries = config.entries || {}; + if (entries.fpr_enabled !== undefined) { + // TODO: Change the assignment of loggingEnabled once the received type is + // known. + settingsServiceInstance.loggingEnabled = + String(entries.fpr_enabled) === 'true'; + } else if (DEFAULT_CONFIGS.loggingEnabled !== undefined) { + // Config retrieved successfully, but there is no fpr_enabled in template. + // Use secondary configs value. + settingsServiceInstance.loggingEnabled = DEFAULT_CONFIGS.loggingEnabled; + } + if (entries.fpr_log_source) { + settingsServiceInstance.logSource = Number(entries.fpr_log_source); + } else if (DEFAULT_CONFIGS.logSource) { + settingsServiceInstance.logSource = DEFAULT_CONFIGS.logSource; + } + + if (entries.fpr_log_endpoint_url) { + settingsServiceInstance.logEndPointUrl = entries.fpr_log_endpoint_url; + } else if (DEFAULT_CONFIGS.logEndPointUrl) { + settingsServiceInstance.logEndPointUrl = DEFAULT_CONFIGS.logEndPointUrl; + } + + // Key from Remote Config has to be non-empty string, otherwsie use local value. + if (entries.fpr_log_transport_key) { + settingsServiceInstance.transportKey = entries.fpr_log_transport_key; + } else if (DEFAULT_CONFIGS.transportKey) { + settingsServiceInstance.transportKey = DEFAULT_CONFIGS.transportKey; + } + + if (entries.fpr_vc_network_request_sampling_rate !== undefined) { + settingsServiceInstance.networkRequestsSamplingRate = Number( + entries.fpr_vc_network_request_sampling_rate + ); + } else if (DEFAULT_CONFIGS.networkRequestsSamplingRate !== undefined) { + settingsServiceInstance.networkRequestsSamplingRate = + DEFAULT_CONFIGS.networkRequestsSamplingRate; + } + if (entries.fpr_vc_trace_sampling_rate !== undefined) { + settingsServiceInstance.tracesSamplingRate = Number( + entries.fpr_vc_trace_sampling_rate + ); + } else if (DEFAULT_CONFIGS.tracesSamplingRate !== undefined) { + settingsServiceInstance.tracesSamplingRate = + DEFAULT_CONFIGS.tracesSamplingRate; + } + // Set the per session trace and network logging flags. + settingsServiceInstance.logTraceAfterSampling = shouldLogAfterSampling( + settingsServiceInstance.tracesSamplingRate + ); + settingsServiceInstance.logNetworkAfterSampling = shouldLogAfterSampling( + settingsServiceInstance.networkRequestsSamplingRate + ); + return config; +} + +function configValid(expiry: string): boolean { + return Number(expiry) > Date.now(); +} + +function shouldLogAfterSampling(samplingRate: number): boolean { + return Math.random() <= samplingRate; +} diff --git a/packages-exp/performance-exp/src/services/settings_service.ts b/packages-exp/performance-exp/src/services/settings_service.ts new file mode 100644 index 00000000000..03540845883 --- /dev/null +++ b/packages-exp/performance-exp/src/services/settings_service.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseApp } from '@firebase/app-types'; +import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; +import { FirebaseInstallations } from '@firebase/installations-types'; +import { mergeStrings } from '../utils/string_merger'; + +let settingsServiceInstance: SettingsService | undefined; + +export class SettingsService { + // The variable which controls logging of automatic traces and HTTP/S network monitoring. + instrumentationEnabled = true; + + // The variable which controls logging of custom traces. + dataCollectionEnabled = true; + + // Configuration flags set through remote config. + loggingEnabled = false; + // Sampling rate between 0 and 1. + tracesSamplingRate = 1; + networkRequestsSamplingRate = 1; + + // Address of logging service. + logEndPointUrl = + 'https://firebaselogging.googleapis.com/v0cc/log?format=json_proto'; + // Performance event transport endpoint URL which should be compatible with proto3. + // New Address for transport service, not configurable via Remote Config. + flTransportEndpointUrl = mergeStrings( + 'hts/frbslgigp.ogepscmv/ieo/eaylg', + 'tp:/ieaeogn-agolai.o/1frlglgc/o' + ); + + transportKey = mergeStrings('AzSC8r6ReiGqFMyfvgow', 'Iayx0u-XT3vksVM-pIV'); + + // Source type for performance event logs. + logSource = 462; + + // Flags which control per session logging of traces and network requests. + logTraceAfterSampling = false; + logNetworkAfterSampling = false; + + // TTL of config retrieved from remote config in hours. + configTimeToLive = 12; + + firebaseAppInstance!: FirebaseApp; + + installationsService!: FirebaseInstallations; + + getAppId(): string { + const appId = + this.firebaseAppInstance && + this.firebaseAppInstance.options && + this.firebaseAppInstance.options.appId; + if (!appId) { + throw ERROR_FACTORY.create(ErrorCode.NO_APP_ID); + } + return appId; + } + + getProjectId(): string { + const projectId = + this.firebaseAppInstance && + this.firebaseAppInstance.options && + this.firebaseAppInstance.options.projectId; + if (!projectId) { + throw ERROR_FACTORY.create(ErrorCode.NO_PROJECT_ID); + } + return projectId; + } + + getApiKey(): string { + const apiKey = + this.firebaseAppInstance && + this.firebaseAppInstance.options && + this.firebaseAppInstance.options.apiKey; + if (!apiKey) { + throw ERROR_FACTORY.create(ErrorCode.NO_API_KEY); + } + return apiKey; + } + + getFlTransportFullUrl(): string { + return this.flTransportEndpointUrl.concat('?key=', this.transportKey); + } + + static getInstance(): SettingsService { + if (settingsServiceInstance === undefined) { + settingsServiceInstance = new SettingsService(); + } + return settingsServiceInstance; + } +} diff --git a/packages-exp/performance-exp/src/services/transport_service.test.ts b/packages-exp/performance-exp/src/services/transport_service.test.ts new file mode 100644 index 00000000000..7f9a64594ed --- /dev/null +++ b/packages-exp/performance-exp/src/services/transport_service.test.ts @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { stub, useFakeTimers, SinonStub, SinonFakeTimers, match } from 'sinon'; +import { use, expect } from 'chai'; +import * as sinonChai from 'sinon-chai'; +import { + transportHandler, + setupTransportService, + resetTransportService +} from './transport_service'; +import { SettingsService } from './settings_service'; + +use(sinonChai); + +describe('Firebase Performance > transport_service', () => { + let fetchStub: SinonStub<[RequestInfo, RequestInit?], Promise>; + const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000; + const DEFAULT_SEND_INTERVAL_MS = 10 * 1000; + // Starts date at timestamp 1 instead of 0, otherwise it causes validation errors. + let clock: SinonFakeTimers; + const testTransportHandler = transportHandler((...args) => { + return args[0]; + }); + + beforeEach(() => { + fetchStub = stub(window, 'fetch'); + clock = useFakeTimers(1); + setupTransportService(); + }); + + afterEach(() => { + fetchStub.restore(); + clock.restore(); + resetTransportService(); + }); + + it('throws an error when logging an empty message', () => { + expect(() => { + testTransportHandler(''); + }).to.throw; + }); + + it('does not attempt to log an event to cc after INITIAL_SEND_TIME_DELAY_MS if queue is empty', () => { + fetchStub.resolves( + new Response('', { + status: 200, + headers: { 'Content-type': 'application/json' } + }) + ); + + clock.tick(INITIAL_SEND_TIME_DELAY_MS); + expect(fetchStub).to.not.have.been.called; + }); + + it('attempts to log an event to cc after DEFAULT_SEND_INTERVAL_MS if queue not empty', () => { + fetchStub.resolves( + new Response('', { + status: 200, + headers: { 'Content-type': 'application/json' } + }) + ); + + clock.tick(INITIAL_SEND_TIME_DELAY_MS); + testTransportHandler('someEvent'); + clock.tick(DEFAULT_SEND_INTERVAL_MS); + expect(fetchStub).to.have.been.calledOnce; + }); + + it('successful send a meesage to transport', () => { + const transportDelayInterval = 30000; + const setting = SettingsService.getInstance(); + const flTransportFullUrl = + setting.flTransportEndpointUrl + '?key=' + setting.transportKey; + fetchStub.withArgs(flTransportFullUrl, match.any).resolves( + // DELETE_REQUEST means event dispatch is successful. + new Response( + '{\ + "nextRequestWaitMillis": "' + + transportDelayInterval + + '",\ + "logResponseDetails": [\ + {\ + "responseAction": "DELETE_REQUEST"\ + }\ + ]\ + }', + { + status: 200, + headers: { 'Content-type': 'application/json' } + } + ) + ); + + testTransportHandler('event1'); + clock.tick(INITIAL_SEND_TIME_DELAY_MS); + expect(fetchStub).to.have.been.calledOnce; + }); +}); diff --git a/packages-exp/performance-exp/src/services/transport_service.ts b/packages-exp/performance-exp/src/services/transport_service.ts new file mode 100644 index 00000000000..5447a77e21c --- /dev/null +++ b/packages-exp/performance-exp/src/services/transport_service.ts @@ -0,0 +1,190 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { SettingsService } from './settings_service'; +import { ERROR_FACTORY, ErrorCode } from '../utils/errors'; +import { consoleLogger } from '../utils/console_logger'; + +const DEFAULT_SEND_INTERVAL_MS = 10 * 1000; +const INITIAL_SEND_TIME_DELAY_MS = 5.5 * 1000; +// If end point does not work, the call will be tried for these many times. +const DEFAULT_REMAINING_TRIES = 3; +let remainingTries = DEFAULT_REMAINING_TRIES; + +interface LogResponseDetails { + responseAction?: string; +} + +interface BatchEvent { + message: string; + eventTime: number; +} + +/* eslint-disable camelcase */ +// CC/Fl accepted log format. +interface TransportBatchLogFormat { + request_time_ms: string; + client_info: ClientInfo; + log_source: number; + log_event: Log[]; +} + +interface ClientInfo { + client_type: number; + js_client_info: {}; +} + +interface Log { + source_extension_json_proto3: string; + event_time_ms: string; +} +/* eslint-enable camelcase */ + +let queue: BatchEvent[] = []; + +let isTransportSetup: boolean = false; + +export function setupTransportService(): void { + if (!isTransportSetup) { + processQueue(INITIAL_SEND_TIME_DELAY_MS); + isTransportSetup = true; + } +} + +/** + * Utilized by testing to clean up message queue and un-initialize transport service. + */ +export function resetTransportService(): void { + isTransportSetup = false; + queue = []; +} + +function processQueue(timeOffset: number): void { + setTimeout(() => { + // If there is no remainingTries left, stop retrying. + if (remainingTries === 0) { + return; + } + + // If there are no events to process, wait for DEFAULT_SEND_INTERVAL_MS and try again. + if (!queue.length) { + return processQueue(DEFAULT_SEND_INTERVAL_MS); + } + + dispatchQueueEvents(); + }, timeOffset); +} + +function dispatchQueueEvents(): void { + // Capture a snapshot of the queue and empty the "official queue". + const staged = [...queue]; + queue = []; + + /* eslint-disable camelcase */ + // We will pass the JSON serialized event to the backend. + const log_event: Log[] = staged.map(evt => ({ + source_extension_json_proto3: evt.message, + event_time_ms: String(evt.eventTime) + })); + + const data: TransportBatchLogFormat = { + request_time_ms: String(Date.now()), + client_info: { + client_type: 1, // 1 is JS + js_client_info: {} + }, + log_source: SettingsService.getInstance().logSource, + log_event + }; + /* eslint-enable camelcase */ + + sendEventsToFl(data, staged).catch(() => { + // If the request fails for some reason, add the events that were attempted + // back to the primary queue to retry later. + queue = [...staged, ...queue]; + remainingTries--; + consoleLogger.info(`Tries left: ${remainingTries}.`); + processQueue(DEFAULT_SEND_INTERVAL_MS); + }); +} + +function sendEventsToFl( + data: TransportBatchLogFormat, + staged: BatchEvent[] +): Promise { + return postToFlEndpoint(data) + .then(res => { + if (!res.ok) { + consoleLogger.info('Call to Firebase backend failed.'); + } + return res.json(); + }) + .then(res => { + // Find the next call wait time from the response. + const transportWait = Number(res.nextRequestWaitMillis); + let requestOffset = DEFAULT_SEND_INTERVAL_MS; + if (!isNaN(transportWait)) { + requestOffset = Math.max(transportWait, requestOffset); + } + + // Delete request if response include RESPONSE_ACTION_UNKNOWN or DELETE_REQUEST action. + // Otherwise, retry request using normal scheduling if response include RETRY_REQUEST_LATER. + const logResponseDetails: LogResponseDetails[] = res.logResponseDetails; + if ( + Array.isArray(logResponseDetails) && + logResponseDetails.length > 0 && + logResponseDetails[0].responseAction === 'RETRY_REQUEST_LATER' + ) { + queue = [...staged, ...queue]; + consoleLogger.info(`Retry transport request later.`); + } + + remainingTries = DEFAULT_REMAINING_TRIES; + // Schedule the next process. + processQueue(requestOffset); + }); +} + +function postToFlEndpoint(data: TransportBatchLogFormat): Promise { + const flTransportFullUrl = SettingsService.getInstance().getFlTransportFullUrl(); + return fetch(flTransportFullUrl, { + method: 'POST', + body: JSON.stringify(data) + }); +} + +function addToQueue(evt: BatchEvent): void { + if (!evt.eventTime || !evt.message) { + throw ERROR_FACTORY.create(ErrorCode.INVALID_CC_LOG); + } + // Add the new event to the queue. + queue = [...queue, evt]; +} + +/** Log handler for cc service to send the performance logs to the server. */ +export function transportHandler( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + serializer: (...args: any[]) => string +): (...args: unknown[]) => void { + return (...args) => { + const message = serializer(...args); + addToQueue({ + message, + eventTime: Date.now() + }); + }; +} diff --git a/packages-exp/performance-exp/src/utils/attribute_utils.test.ts b/packages-exp/performance-exp/src/utils/attribute_utils.test.ts new file mode 100644 index 00000000000..87b202dbc38 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/attribute_utils.test.ts @@ -0,0 +1,215 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 unknown KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { restore, stub } from 'sinon'; +import { expect } from 'chai'; +import { Api } from '../services/api_service'; + +import { + getVisibilityState, + VisibilityState, + getServiceWorkerStatus, + getEffectiveConnectionType, + isValidCustomAttributeName, + isValidCustomAttributeValue +} from './attributes_utils'; + +import '../../test/setup'; + +describe('Firebase Performance > attribute_utils', () => { + describe('#getServiceWorkerStatus', () => { + it('returns unsupported when service workers unsupported', () => { + stub(Api, 'getInstance').returns(({ + navigator: {} + } as unknown) as Api); + + expect(getServiceWorkerStatus()).to.be.eql(1); + }); + + it('returns controlled when service workers controlled', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + serviceWorker: { + controller: {} + } + } + } as unknown) as Api); + + expect(getServiceWorkerStatus()).to.be.eql(2); + }); + + it('returns uncontrolled when service workers uncontrolled', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + serviceWorker: {} + } + } as unknown) as Api); + + expect(getServiceWorkerStatus()).to.be.eql(3); + }); + }); + + describe('#getVisibilityState', () => { + afterEach(() => { + restore(); + }); + + it('returns visible when document is visible', () => { + stub(Api, 'getInstance').returns(({ + document: { + visibilityState: 'visible' + } + } as unknown) as Api); + expect(getVisibilityState()).to.be.eql(VisibilityState.VISIBLE); + }); + + it('returns hidden when document is hidden', () => { + stub(Api, 'getInstance').returns(({ + document: { + visibilityState: 'hidden' + } + } as unknown) as Api); + expect(getVisibilityState()).to.be.eql(VisibilityState.HIDDEN); + }); + + it('returns unknown when document is unknown', () => { + stub(Api, 'getInstance').returns(({ + document: { + visibilityState: 'unknown' + } + } as unknown) as Api); + expect(getVisibilityState()).to.be.eql(VisibilityState.UNKNOWN); + }); + }); + + describe('#getEffectiveConnectionType', () => { + afterEach(() => { + restore(); + }); + + it('returns EffectiveConnectionType.CONNECTION_SLOW_2G when slow-2g', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: { + effectiveType: 'slow-2g' + } + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(1); + }); + + it('returns EffectiveConnectionType.CONNECTION_2G when 2g', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: { + effectiveType: '2g' + } + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(2); + }); + + it('returns EffectiveConnectionType.CONNECTION_3G when 3g', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: { + effectiveType: '3g' + } + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(3); + }); + + it('returns EffectiveConnectionType.CONNECTION_4G when 4g', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: { + effectiveType: '4g' + } + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(4); + }); + + it('returns EffectiveConnectionType.UNKNOWN when unknown connection type', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: { + effectiveType: '5g' + } + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(0); + }); + + it('returns EffectiveConnectionType.UNKNOWN when no effective type', () => { + stub(Api, 'getInstance').returns(({ + navigator: { + connection: {} + } + } as unknown) as Api); + expect(getEffectiveConnectionType()).to.be.eql(0); + }); + }); + + describe('#isValidCustomAttributeName', () => { + it('returns true when name is valid', () => { + expect(isValidCustomAttributeName('validCustom_Attribute_Name')).to.be + .true; + }); + + it('returns false when name is blank', () => { + expect(isValidCustomAttributeName('')).to.be.false; + }); + + it('returns false when name is too long', () => { + expect( + isValidCustomAttributeName('invalid_custom_name_over_forty_characters') + ).to.be.false; + }); + + it('returns false when name starts with a reserved prefix', () => { + expect(isValidCustomAttributeName('firebase_invalidCustomName')).to.be + .false; + }); + + it('returns false when name does not begin with a letter', () => { + expect(isValidCustomAttributeName('_invalidCustomName')).to.be.false; + }); + + it('returns false when name contains prohibited characters', () => { + expect(isValidCustomAttributeName('invalidCustomName&')).to.be.false; + }); + }); + + describe('#isValidCustomAttributeValue', () => { + it('returns true when value is valid', () => { + expect(isValidCustomAttributeValue('valid_attribute_value')).to.be.true; + }); + + it('returns false when value is blank', () => { + expect(isValidCustomAttributeValue('')).to.be.false; + }); + + it('returns false when value is too long', () => { + const longAttributeValue = + 'too_long_attribute_value_over_one_hundred_characters_too_long_attribute_value_over_one_' + + 'hundred_charac'; + expect(isValidCustomAttributeValue(longAttributeValue)).to.be.false; + }); + }); +}); diff --git a/packages-exp/performance-exp/src/utils/attributes_utils.ts b/packages-exp/performance-exp/src/utils/attributes_utils.ts new file mode 100644 index 00000000000..c060f1de1f2 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/attributes_utils.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { Api } from '../services/api_service'; + +// The values and orders of the following enums should not be changed. +const enum ServiceWorkerStatus { + UNKNOWN = 0, + UNSUPPORTED = 1, + CONTROLLED = 2, + UNCONTROLLED = 3 +} + +export enum VisibilityState { + UNKNOWN = 0, + VISIBLE = 1, + HIDDEN = 2 +} + +const enum EffectiveConnectionType { + UNKNOWN = 0, + CONNECTION_SLOW_2G = 1, + CONNECTION_2G = 2, + CONNECTION_3G = 3, + CONNECTION_4G = 4 +} + +/** + * NetworkInformation + * + * ref: https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation + */ +interface NetworkInformation { + readonly effectiveType?: 'slow-2g' | '2g' | '3g' | '4g'; +} + +interface NavigatorWithConnection extends Navigator { + readonly connection: NetworkInformation; +} + +const RESERVED_ATTRIBUTE_PREFIXES = ['firebase_', 'google_', 'ga_']; +const ATTRIBUTE_FORMAT_REGEX = new RegExp('^[a-zA-Z]\\w*$'); +const MAX_ATTRIBUTE_NAME_LENGTH = 40; +const MAX_ATTRIBUTE_VALUE_LENGTH = 100; + +export function getServiceWorkerStatus(): ServiceWorkerStatus { + const navigator = Api.getInstance().navigator; + if ('serviceWorker' in navigator) { + if (navigator.serviceWorker.controller) { + return ServiceWorkerStatus.CONTROLLED; + } else { + return ServiceWorkerStatus.UNCONTROLLED; + } + } else { + return ServiceWorkerStatus.UNSUPPORTED; + } +} + +export function getVisibilityState(): VisibilityState { + const document = Api.getInstance().document; + const visibilityState = document.visibilityState; + switch (visibilityState) { + case 'visible': + return VisibilityState.VISIBLE; + case 'hidden': + return VisibilityState.HIDDEN; + default: + return VisibilityState.UNKNOWN; + } +} + +export function getEffectiveConnectionType(): EffectiveConnectionType { + const navigator = Api.getInstance().navigator; + const navigatorConnection = (navigator as NavigatorWithConnection).connection; + const effectiveType = + navigatorConnection && navigatorConnection.effectiveType; + switch (effectiveType) { + case 'slow-2g': + return EffectiveConnectionType.CONNECTION_SLOW_2G; + case '2g': + return EffectiveConnectionType.CONNECTION_2G; + case '3g': + return EffectiveConnectionType.CONNECTION_3G; + case '4g': + return EffectiveConnectionType.CONNECTION_4G; + default: + return EffectiveConnectionType.UNKNOWN; + } +} + +export function isValidCustomAttributeName(name: string): boolean { + if (name.length === 0 || name.length > MAX_ATTRIBUTE_NAME_LENGTH) { + return false; + } + const matchesReservedPrefix = RESERVED_ATTRIBUTE_PREFIXES.some(prefix => + name.startsWith(prefix) + ); + return !matchesReservedPrefix && !!name.match(ATTRIBUTE_FORMAT_REGEX); +} + +export function isValidCustomAttributeValue(value: string): boolean { + return value.length !== 0 && value.length <= MAX_ATTRIBUTE_VALUE_LENGTH; +} diff --git a/packages-exp/performance-exp/src/utils/console_logger.ts b/packages-exp/performance-exp/src/utils/console_logger.ts new file mode 100644 index 00000000000..8ba63d2879e --- /dev/null +++ b/packages-exp/performance-exp/src/utils/console_logger.ts @@ -0,0 +1,22 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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, LogLevel } from '@firebase/logger'; +import { SERVICE_NAME } from '../constants'; + +export const consoleLogger = new Logger(SERVICE_NAME); +consoleLogger.logLevel = LogLevel.INFO; diff --git a/packages-exp/performance-exp/src/utils/errors.ts b/packages-exp/performance-exp/src/utils/errors.ts new file mode 100644 index 00000000000..44be2b75596 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/errors.ts @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { ErrorFactory } from '@firebase/util'; +import { SERVICE, SERVICE_NAME } from '../constants'; + +export const enum ErrorCode { + TRACE_STARTED_BEFORE = 'trace started', + TRACE_STOPPED_BEFORE = 'trace stopped', + NO_WINDOW = 'no window', + NO_APP_ID = 'no app id', + NO_PROJECT_ID = 'no project id', + NO_API_KEY = 'no api key', + INVALID_CC_LOG = 'invalid cc log', + FB_NOT_DEFAULT = 'FB not default', + RC_NOT_OK = 'RC response not ok', + INVALID_ATTRIBUTE_NAME = 'invalid attribute name', + INVALID_ATTRIBUTE_VALUE = 'invalid attribute value', + INVALID_CUSTOM_METRIC_NAME = 'invalid custom metric name', + INVALID_STRING_MERGER_PARAMETER = 'invalid String merger input' +} + +const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { + [ErrorCode.TRACE_STARTED_BEFORE]: 'Trace {$traceName} was started before.', + [ErrorCode.TRACE_STOPPED_BEFORE]: 'Trace {$traceName} is not running.', + [ErrorCode.NO_WINDOW]: 'Window is not available.', + [ErrorCode.NO_APP_ID]: 'App id is not available.', + [ErrorCode.NO_PROJECT_ID]: 'Project id is not available.', + [ErrorCode.NO_API_KEY]: 'Api key is not available.', + [ErrorCode.INVALID_CC_LOG]: 'Attempted to queue invalid cc event', + [ErrorCode.FB_NOT_DEFAULT]: + 'Performance can only start when Firebase app instance is the default one.', + [ErrorCode.RC_NOT_OK]: 'RC response is not ok', + [ErrorCode.INVALID_ATTRIBUTE_NAME]: + 'Attribute name {$attributeName} is invalid.', + [ErrorCode.INVALID_ATTRIBUTE_VALUE]: + 'Attribute value {$attributeValue} is invalid.', + [ErrorCode.INVALID_CUSTOM_METRIC_NAME]: + 'Custom metric name {$customMetricName} is invalid', + [ErrorCode.INVALID_STRING_MERGER_PARAMETER]: + 'Input for String merger is invalid, contact support team to resolve.' +}; + +interface ErrorParams { + [ErrorCode.TRACE_STARTED_BEFORE]: { traceName: string }; + [ErrorCode.TRACE_STOPPED_BEFORE]: { traceName: string }; + [ErrorCode.INVALID_ATTRIBUTE_NAME]: { attributeName: string }; + [ErrorCode.INVALID_ATTRIBUTE_VALUE]: { attributeValue: string }; + [ErrorCode.INVALID_CUSTOM_METRIC_NAME]: { customMetricName: string }; +} + +export const ERROR_FACTORY = new ErrorFactory( + SERVICE, + SERVICE_NAME, + ERROR_DESCRIPTION_MAP +); diff --git a/packages-exp/performance-exp/src/utils/metric_utils.test.ts b/packages-exp/performance-exp/src/utils/metric_utils.test.ts new file mode 100644 index 00000000000..0dee88521e3 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/metric_utils.test.ts @@ -0,0 +1,93 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 unknown KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import { isValidMetricName } from './metric_utils'; +import { + FIRST_PAINT_COUNTER_NAME, + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + FIRST_INPUT_DELAY_COUNTER_NAME +} from '../constants'; +import '../../test/setup'; + +describe('Firebase Performance > metric_utils', () => { + describe('#isValidMetricName', () => { + it('returns true when name is valid', () => { + expect(isValidMetricName('validCustom_Metric_Name')).to.be.true; + }); + + it('returns false when name is blank', () => { + expect(isValidMetricName('')).to.be.false; + }); + + it('returns false when name is too long', () => { + const longMetricName = + 'too_long_metric_name_over_one_hundred_characters_too_long_metric_name_over_one_' + + 'hundred_characters_too'; + expect(isValidMetricName(longMetricName)).to.be.false; + }); + + it('returns false when name starts with a reserved prefix', () => { + expect(isValidMetricName('_invalidMetricName')).to.be.false; + }); + + it('returns true for first paint metric', () => { + expect( + isValidMetricName(FIRST_PAINT_COUNTER_NAME, '_wt_http://example.com') + ).to.be.true; + }); + + it('returns true for first contentful paint metric', () => { + expect( + isValidMetricName( + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + '_wt_http://example.com' + ) + ).to.be.true; + }); + + it('returns true for first input delay metric', () => { + expect( + isValidMetricName( + FIRST_INPUT_DELAY_COUNTER_NAME, + '_wt_http://example.com' + ) + ).to.be.true; + }); + + it('returns false if first paint metric name is used outside of page load traces', () => { + expect(isValidMetricName(FIRST_PAINT_COUNTER_NAME, 'some_randome_trace')) + .to.be.false; + }); + + it('returns false if first contentful paint metric name is used outside of page load traces', () => { + expect( + isValidMetricName( + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + 'some_randome_trace' + ) + ).to.be.false; + }); + + it('returns false if first input delay metric name is used outside of page load traces', () => { + expect( + isValidMetricName(FIRST_INPUT_DELAY_COUNTER_NAME, 'some_randome_trace') + ).to.be.false; + }); + }); +}); diff --git a/packages-exp/performance-exp/src/utils/metric_utils.ts b/packages-exp/performance-exp/src/utils/metric_utils.ts new file mode 100644 index 00000000000..aad0357114c --- /dev/null +++ b/packages-exp/performance-exp/src/utils/metric_utils.ts @@ -0,0 +1,64 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { + FIRST_PAINT_COUNTER_NAME, + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + FIRST_INPUT_DELAY_COUNTER_NAME, + OOB_TRACE_PAGE_LOAD_PREFIX +} from '../constants'; +import { consoleLogger } from '../utils/console_logger'; + +const MAX_METRIC_NAME_LENGTH = 100; +const RESERVED_AUTO_PREFIX = '_'; +const oobMetrics = [ + FIRST_PAINT_COUNTER_NAME, + FIRST_CONTENTFUL_PAINT_COUNTER_NAME, + FIRST_INPUT_DELAY_COUNTER_NAME +]; + +/** + * Returns true if the metric is custom and does not start with reserved prefix, or if + * the metric is one of out of the box page load trace metrics. + */ +export function isValidMetricName(name: string, traceName?: string): boolean { + if (name.length === 0 || name.length > MAX_METRIC_NAME_LENGTH) { + return false; + } + return ( + (traceName && + traceName.startsWith(OOB_TRACE_PAGE_LOAD_PREFIX) && + oobMetrics.indexOf(name) > -1) || + !name.startsWith(RESERVED_AUTO_PREFIX) + ); +} + +/** + * Converts the provided value to an integer value to be used in case of a metric. + * @param providedValue Provided number value of the metric that needs to be converted to an integer. + * + * @returns Converted integer number to be set for the metric. + */ +export function convertMetricValueToInteger(providedValue: number): number { + const valueAsInteger: number = Math.floor(providedValue); + if (valueAsInteger < providedValue) { + consoleLogger.info( + `Metric value should be an Integer, setting the value as : ${valueAsInteger}.` + ); + } + return valueAsInteger; +} diff --git a/packages-exp/performance-exp/src/utils/string_merger.test.ts b/packages-exp/performance-exp/src/utils/string_merger.test.ts new file mode 100644 index 00000000000..e192f8438f7 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/string_merger.test.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 unknown KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect } from 'chai'; + +import { mergeStrings } from './string_merger'; +import { FirebaseError } from '@firebase/util'; +// import { ERROR_FACTORY, ErrorCode } from './errors'; +import '../../test/setup'; + +describe('Firebase Performance > string_merger', () => { + describe('#mergeStrings', () => { + it('Throws exception when string length has | diff | > 1', () => { + // const expectedError = ERROR_FACTORY.create(ErrorCode.INVALID_STRING_MERGER_PARAMETER); + expect(() => mergeStrings('', '123')).to.throw( + FirebaseError, + 'performance/invalid String merger input' + ); + }); + + it('returns empty string when both inputs are empty', () => { + expect(mergeStrings('', '')).equal(''); + }); + + it('returns merge result string when both inputs have same length', () => { + expect(mergeStrings('12345', 'abcde')).equal('1a2b3c4d5e'); + }); + + it('returns merge result string when input length diff == 1', () => { + expect(() => mergeStrings('1234', 'abcde')).to.throw( + FirebaseError, + 'performance/invalid String merger input' + ); + }); + + it('returns merge result string when input length diff == -1', () => { + expect(mergeStrings('12345', 'abcd')).equal('1a2b3c4d5'); + }); + }); +}); diff --git a/packages-exp/performance-exp/src/utils/string_merger.ts b/packages-exp/performance-exp/src/utils/string_merger.ts new file mode 100644 index 00000000000..d26295f7b11 --- /dev/null +++ b/packages-exp/performance-exp/src/utils/string_merger.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { ERROR_FACTORY, ErrorCode } from './errors'; + +export function mergeStrings(part1: string, part2: string): string { + const sizeDiff = part1.length - part2.length; + if (sizeDiff < 0 || sizeDiff > 1) { + throw ERROR_FACTORY.create(ErrorCode.INVALID_STRING_MERGER_PARAMETER); + } + + const resultArray = []; + for (let i = 0; i < part1.length; i++) { + resultArray.push(part1.charAt(i)); + if (part2.length > i) { + resultArray.push(part2.charAt(i)); + } + } + + return resultArray.join(''); +} diff --git a/packages-exp/performance-exp/test/setup.ts b/packages-exp/performance-exp/test/setup.ts new file mode 100644 index 00000000000..1b4641c59a8 --- /dev/null +++ b/packages-exp/performance-exp/test/setup.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { restore } from 'sinon'; +import { use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinonChai from 'sinon-chai'; + +use(chaiAsPromised); +use(sinonChai); +afterEach(() => { + restore(); +}); diff --git a/packages-exp/performance-exp/tsconfig.json b/packages-exp/performance-exp/tsconfig.json new file mode 100644 index 00000000000..a3c61dfddfd --- /dev/null +++ b/packages-exp/performance-exp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "exclude": ["dist/**/*"] +} diff --git a/packages-exp/performance-types-exp/index.d.ts b/packages-exp/performance-types-exp/index.d.ts new file mode 100644 index 00000000000..55463ee8434 --- /dev/null +++ b/packages-exp/performance-types-exp/index.d.ts @@ -0,0 +1,119 @@ +/** + * @license + * Copyright 2017 Google LLC + * + * 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. + */ + +export interface FirebasePerformance { + /** + * Creates an uninitialized instance of trace and returns it. + * + * @param traceName The name of trace instance. + * @return The trace instance. + */ + trace(traceName: string): PerformanceTrace; + + /** + * Controls the logging of automatic traces and HTTP/S network monitoring. + */ + instrumentationEnabled: boolean; + /** + * Controls the logging of custom traces. + */ + dataCollectionEnabled: boolean; +} + +export interface PerformanceTrace { + /** + * Starts the timing for the trace instance. + */ + start(): void; + /** + * Stops the timing of the trace instance and logs the data of the instance. + */ + stop(): void; + /** + * Records a trace from given parameters. This provides a direct way to use trace without a need to + * start/stop. This is useful for use cases in which the trace cannot directly be used + * (e.g. if the duration was captured before the Performance SDK was loaded). + * + * @param startTime trace start time since epoch in millisec. + * @param duration The duraction of the trace in millisec. + * @param options An object which can optionally hold maps of custom metrics and + * custom attributes. + */ + record( + startTime: number, + duration: number, + options?: { + metrics?: { [key: string]: number }; + attributes?: { [key: string]: string }; + } + ): void; + /** + * Adds to the value of a custom metric. If a custom metric with the provided name does not + * exist, it creates one with that name and the value equal to the given number. The value will be floored down to an + * integer. + * + * @param metricName The name of the custom metric. + * @param num The number to be added to the value of the custom metric. If not provided, it + * uses a default value of one. + */ + incrementMetric(metricName: string, num?: number): void; + /** + * Sets the value of the specified custom metric to the given number regardless of whether + * a metric with that name already exists on the trace instance or not. The value will be floored down to an + * integer. + * + * @param metricName Name of the custom metric. + * @param num Value to of the custom metric. + */ + putMetric(metricName: string, num: number): void; + /** + * Returns the value of the custom metric by that name. If a custom metric with that name does + * not exist will return zero. + * + * @param metricName Name of the custom metric. + */ + getMetric(metricName: string): number; + /** + * Set a custom attribute of a trace to a certain value. + * + * @param attr Name of the custom attribute. + * @param value Value of the custom attribute. + */ + putAttribute(attr: string, value: string): void; + /** + * Retrieves the value which a custom attribute is set to. + * + * @param attr Name of the custom attribute. + */ + getAttribute(attr: string): string | undefined; + /** + * Removes the specified custom attribute from a trace instance. + * + * @param attr Name of the custom attribute. + */ + removeAttribute(attr: string): void; + /** + * Returns a map of all custom attributes of a trace instance. + */ + getAttributes(): { [key: string]: string }; +} + +declare module '@firebase/component' { + interface NameServiceMapping { + 'performance': FirebasePerformance; + } +} diff --git a/packages-exp/performance-types-exp/package.json b/packages-exp/performance-types-exp/package.json new file mode 100644 index 00000000000..892aefaeb03 --- /dev/null +++ b/packages-exp/performance-types-exp/package.json @@ -0,0 +1,25 @@ +{ + "name": "@firebase/performance-types-exp", + "version": "0.0.13", + "description": "@firebase/performance Types", + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "scripts": { + "test": "tsc", + "test:ci": "node ../../scripts/run_tests_in_ci.js" + }, + "files": [ + "index.d.ts" + ], + "devDependencies": { + "typescript": "4.0.2" + }, + "repository": { + "directory": "packages/performance-types-exp", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + } +} diff --git a/packages-exp/performance-types-exp/tsconfig.json b/packages-exp/performance-types-exp/tsconfig.json new file mode 100644 index 00000000000..09f747b4d46 --- /dev/null +++ b/packages-exp/performance-types-exp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "exclude": [ + "dist/**/*" + ] +} From 96f58d5ebacff70f2706c2b2876f948dec2e7e80 Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Tue, 15 Sep 2020 16:59:25 -0700 Subject: [PATCH 2/6] Removing CHANGELOG.md --- packages-exp/performance-exp/CHANGELOG.md | 53 ----------------------- 1 file changed, 53 deletions(-) delete mode 100644 packages-exp/performance-exp/CHANGELOG.md diff --git a/packages-exp/performance-exp/CHANGELOG.md b/packages-exp/performance-exp/CHANGELOG.md deleted file mode 100644 index ddfcb314890..00000000000 --- a/packages-exp/performance-exp/CHANGELOG.md +++ /dev/null @@ -1,53 +0,0 @@ -# @firebase/performance - -## 0.4.1 - -### Patch Changes - -- Updated dependencies [[`da1c7df79`](https://github.com/firebase/firebase-js-sdk/commit/da1c7df7982b08bbef82fcc8d93255f3e2d23cca), [`fb3b095e4`](https://github.com/firebase/firebase-js-sdk/commit/fb3b095e4b7c8f57fdb3172bc039c84576abf290)]: - - @firebase/component@0.1.19 - - @firebase/util@0.3.2 - - @firebase/installations@0.4.17 - -## 0.4.0 - -### Minor Changes - -- [`67501b980`](https://github.com/firebase/firebase-js-sdk/commit/67501b9806c7014738080bc0be945b2c0748c17e) [#3424](https://github.com/firebase/firebase-js-sdk/pull/3424) - Issue 2393 - Add environment check to Performance Module - -## 0.3.11 - -### Patch Changes - -- Updated dependencies [[`d4ca3da0`](https://github.com/firebase/firebase-js-sdk/commit/d4ca3da0a59fcea1261ba69d7eb663bba38d3089)]: - - @firebase/util@0.3.1 - - @firebase/component@0.1.18 - - @firebase/installations@0.4.16 - -## 0.3.10 - -### Patch Changes - -- Updated dependencies [[`a87676b8`](https://github.com/firebase/firebase-js-sdk/commit/a87676b84b78ccc2f057a22eb947a5d13402949c)]: - - @firebase/util@0.3.0 - - @firebase/component@0.1.17 - - @firebase/installations@0.4.15 - -## 0.3.9 - -### Patch Changes - -- [`a754645e`](https://github.com/firebase/firebase-js-sdk/commit/a754645ec2be1b8c205f25f510196eee298b0d6e) [#3297](https://github.com/firebase/firebase-js-sdk/pull/3297) Thanks [@renovate](https://github.com/apps/renovate)! - Update dependency typescript to v3.9.5 - -- Updated dependencies [[`a754645e`](https://github.com/firebase/firebase-js-sdk/commit/a754645ec2be1b8c205f25f510196eee298b0d6e)]: - - @firebase/component@0.1.16 - - @firebase/installations@0.4.14 - - @firebase/logger@0.2.6 - -## 0.3.0 - -- [changed] Updated internal performance event transport mechanism. - -## 0.2.30 - -- [changed] Internal transport protocol update from proto2 to proto3. From d96ad31812c57dbeacf8c7cd46efe0fc2a7b554e Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Tue, 15 Sep 2020 17:17:24 -0700 Subject: [PATCH 3/6] Updated the copyright year on all files --- packages-exp/performance-exp/README.md | 2 +- packages-exp/performance-exp/index.ts | 2 +- .../src/controllers/perf.test.ts | 2 +- .../performance-exp/src/controllers/perf.ts | 2 +- packages-exp/performance-exp/src/index.ts | 73 +++++++++++++++++++ .../src/resources/network_request.test.ts | 2 +- .../src/resources/network_request.ts | 2 +- .../src/resources/trace.test.ts | 2 +- .../performance-exp/src/resources/trace.ts | 2 +- .../src/services/api_service.test.ts | 2 +- .../src/services/api_service.ts | 2 +- .../src/services/iid_service.ts | 2 +- .../services/initialization_service.test.ts | 2 +- .../src/services/initialization_service.ts | 2 +- .../services/oob_resources_service.test.ts | 2 +- .../src/services/oob_resources_service.ts | 2 +- .../src/services/perf_logger.test.ts | 2 +- .../src/services/perf_logger.ts | 2 +- .../services/remote_config_service.test.ts | 2 +- .../src/services/remote_config_service.ts | 2 +- .../src/services/transport_service.test.ts | 2 +- .../src/services/transport_service.ts | 2 +- .../src/utils/attribute_utils.test.ts | 2 +- .../src/utils/attributes_utils.ts | 2 +- .../src/utils/console_logger.ts | 2 +- .../performance-exp/src/utils/errors.ts | 2 +- .../src/utils/metric_utils.test.ts | 2 +- .../performance-exp/src/utils/metric_utils.ts | 2 +- .../src/utils/string_merger.test.ts | 2 +- .../src/utils/string_merger.ts | 2 +- packages-exp/performance-exp/test/setup.ts | 2 +- 31 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 packages-exp/performance-exp/src/index.ts diff --git a/packages-exp/performance-exp/README.md b/packages-exp/performance-exp/README.md index 5c83dbc51b7..6193537a9a5 100644 --- a/packages-exp/performance-exp/README.md +++ b/packages-exp/performance-exp/README.md @@ -1,4 +1,4 @@ -# @firebase/performance +# @firebase/performance-exp This is the Firebase Performance component of the Firebase JS SDK. diff --git a/packages-exp/performance-exp/index.ts b/packages-exp/performance-exp/index.ts index 6829a9154a1..61e0fb8c3a1 100644 --- a/packages-exp/performance-exp/index.ts +++ b/packages-exp/performance-exp/index.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/controllers/perf.test.ts b/packages-exp/performance-exp/src/controllers/perf.test.ts index 6d3a24e723f..5908d81680a 100644 --- a/packages-exp/performance-exp/src/controllers/perf.test.ts +++ b/packages-exp/performance-exp/src/controllers/perf.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/controllers/perf.ts b/packages-exp/performance-exp/src/controllers/perf.ts index 9793f614dd4..288d5389445 100644 --- a/packages-exp/performance-exp/src/controllers/perf.ts +++ b/packages-exp/performance-exp/src/controllers/perf.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/index.ts b/packages-exp/performance-exp/src/index.ts new file mode 100644 index 00000000000..238b76118ee --- /dev/null +++ b/packages-exp/performance-exp/src/index.ts @@ -0,0 +1,73 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { FirebaseApp } from '@firebase/app-types-exp'; +import { FirebasePerformance } from '@firebase/performance-types-exp'; +import { ERROR_FACTORY, ErrorCode } from './utils/errors'; +import { setupApi } from './services/api_service'; +import { PerformanceController } from './controllers/perf'; +import { + _registerComponent, + _getProvider, + registerVersion +} from '@firebase/app-exp'; +import { + InstanceFactory, + ComponentContainer, + Component, + ComponentType +} from '@firebase/component'; +import { SettingsService } from './services/settings_service'; +import { name, version } from '../package.json'; + +const DEFAULT_ENTRY_NAME = '[DEFAULT]'; + +export function getPerformance(app: FirebaseApp): FirebasePerformance { + const provider = _getProvider(app, 'performance-exp'); + const perfInstance = provider.getImmediate() as PerformanceController; + return perfInstance; +} + +const factory: InstanceFactory<'performance-exp'> = ( + container: ComponentContainer +) => { + // Dependencies + const app = container.getProvider('app-exp').getImmediate(); + const installations = container + .getProvider('installations-exp-internal') + .getImmediate(); + + if (app.name !== DEFAULT_ENTRY_NAME) { + throw ERROR_FACTORY.create(ErrorCode.FB_NOT_DEFAULT); + } + if (typeof window === 'undefined') { + throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); + } + setupApi(window); + SettingsService.getInstance().firebaseAppInstance = app; + SettingsService.getInstance().installationsService = installations; + return new PerformanceController(app); +}; + +export function registerPerformance(): void { + _registerComponent( + new Component('performance-exp', factory, ComponentType.PUBLIC) + ); +} + +registerPerformance(); +registerVersion(name, version); diff --git a/packages-exp/performance-exp/src/resources/network_request.test.ts b/packages-exp/performance-exp/src/resources/network_request.test.ts index 2484b08fdf2..528f8b5bd11 100644 --- a/packages-exp/performance-exp/src/resources/network_request.test.ts +++ b/packages-exp/performance-exp/src/resources/network_request.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/resources/network_request.ts b/packages-exp/performance-exp/src/resources/network_request.ts index ac7cfe114fe..335291092aa 100644 --- a/packages-exp/performance-exp/src/resources/network_request.ts +++ b/packages-exp/performance-exp/src/resources/network_request.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/resources/trace.test.ts b/packages-exp/performance-exp/src/resources/trace.test.ts index f9df8565443..6839326da5e 100644 --- a/packages-exp/performance-exp/src/resources/trace.test.ts +++ b/packages-exp/performance-exp/src/resources/trace.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/resources/trace.ts b/packages-exp/performance-exp/src/resources/trace.ts index be34129690c..58789b59862 100644 --- a/packages-exp/performance-exp/src/resources/trace.ts +++ b/packages-exp/performance-exp/src/resources/trace.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/api_service.test.ts b/packages-exp/performance-exp/src/services/api_service.test.ts index 32ac012ffc3..251c6903896 100644 --- a/packages-exp/performance-exp/src/services/api_service.test.ts +++ b/packages-exp/performance-exp/src/services/api_service.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/api_service.ts b/packages-exp/performance-exp/src/services/api_service.ts index df719e8fb16..8da91188ccc 100644 --- a/packages-exp/performance-exp/src/services/api_service.ts +++ b/packages-exp/performance-exp/src/services/api_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/iid_service.ts b/packages-exp/performance-exp/src/services/iid_service.ts index 5213900ba47..c880c9b0672 100644 --- a/packages-exp/performance-exp/src/services/iid_service.ts +++ b/packages-exp/performance-exp/src/services/iid_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/initialization_service.test.ts b/packages-exp/performance-exp/src/services/initialization_service.test.ts index 832081bdc3c..7b0e16505fc 100644 --- a/packages-exp/performance-exp/src/services/initialization_service.test.ts +++ b/packages-exp/performance-exp/src/services/initialization_service.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/initialization_service.ts b/packages-exp/performance-exp/src/services/initialization_service.ts index 96226867122..f96af6688d4 100644 --- a/packages-exp/performance-exp/src/services/initialization_service.ts +++ b/packages-exp/performance-exp/src/services/initialization_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.test.ts b/packages-exp/performance-exp/src/services/oob_resources_service.test.ts index 9534d7aa087..ada9d2367df 100644 --- a/packages-exp/performance-exp/src/services/oob_resources_service.test.ts +++ b/packages-exp/performance-exp/src/services/oob_resources_service.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/oob_resources_service.ts b/packages-exp/performance-exp/src/services/oob_resources_service.ts index 0e01236903e..12182ce3418 100644 --- a/packages-exp/performance-exp/src/services/oob_resources_service.ts +++ b/packages-exp/performance-exp/src/services/oob_resources_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/perf_logger.test.ts b/packages-exp/performance-exp/src/services/perf_logger.test.ts index 76148eeca0b..75ecd986e3a 100644 --- a/packages-exp/performance-exp/src/services/perf_logger.test.ts +++ b/packages-exp/performance-exp/src/services/perf_logger.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/perf_logger.ts b/packages-exp/performance-exp/src/services/perf_logger.ts index 2d2daa22fca..6109cec147c 100644 --- a/packages-exp/performance-exp/src/services/perf_logger.ts +++ b/packages-exp/performance-exp/src/services/perf_logger.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/remote_config_service.test.ts b/packages-exp/performance-exp/src/services/remote_config_service.test.ts index 7bbcf3722d5..3b93c5aa742 100644 --- a/packages-exp/performance-exp/src/services/remote_config_service.test.ts +++ b/packages-exp/performance-exp/src/services/remote_config_service.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/remote_config_service.ts b/packages-exp/performance-exp/src/services/remote_config_service.ts index 71f5266c805..1721edbcc5d 100644 --- a/packages-exp/performance-exp/src/services/remote_config_service.ts +++ b/packages-exp/performance-exp/src/services/remote_config_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/transport_service.test.ts b/packages-exp/performance-exp/src/services/transport_service.test.ts index 7f9a64594ed..871f9bb2782 100644 --- a/packages-exp/performance-exp/src/services/transport_service.test.ts +++ b/packages-exp/performance-exp/src/services/transport_service.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/services/transport_service.ts b/packages-exp/performance-exp/src/services/transport_service.ts index 5447a77e21c..cc1328fe637 100644 --- a/packages-exp/performance-exp/src/services/transport_service.ts +++ b/packages-exp/performance-exp/src/services/transport_service.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/attribute_utils.test.ts b/packages-exp/performance-exp/src/utils/attribute_utils.test.ts index 87b202dbc38..bda20b3e165 100644 --- a/packages-exp/performance-exp/src/utils/attribute_utils.test.ts +++ b/packages-exp/performance-exp/src/utils/attribute_utils.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/attributes_utils.ts b/packages-exp/performance-exp/src/utils/attributes_utils.ts index c060f1de1f2..ef3f499e23b 100644 --- a/packages-exp/performance-exp/src/utils/attributes_utils.ts +++ b/packages-exp/performance-exp/src/utils/attributes_utils.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/console_logger.ts b/packages-exp/performance-exp/src/utils/console_logger.ts index 8ba63d2879e..2f97aeb386b 100644 --- a/packages-exp/performance-exp/src/utils/console_logger.ts +++ b/packages-exp/performance-exp/src/utils/console_logger.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/errors.ts b/packages-exp/performance-exp/src/utils/errors.ts index 44be2b75596..37ffb115e1a 100644 --- a/packages-exp/performance-exp/src/utils/errors.ts +++ b/packages-exp/performance-exp/src/utils/errors.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/metric_utils.test.ts b/packages-exp/performance-exp/src/utils/metric_utils.test.ts index 0dee88521e3..fea213911f7 100644 --- a/packages-exp/performance-exp/src/utils/metric_utils.test.ts +++ b/packages-exp/performance-exp/src/utils/metric_utils.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/metric_utils.ts b/packages-exp/performance-exp/src/utils/metric_utils.ts index aad0357114c..9bbc4886aef 100644 --- a/packages-exp/performance-exp/src/utils/metric_utils.ts +++ b/packages-exp/performance-exp/src/utils/metric_utils.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/string_merger.test.ts b/packages-exp/performance-exp/src/utils/string_merger.test.ts index e192f8438f7..ded5472e580 100644 --- a/packages-exp/performance-exp/src/utils/string_merger.test.ts +++ b/packages-exp/performance-exp/src/utils/string_merger.test.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/utils/string_merger.ts b/packages-exp/performance-exp/src/utils/string_merger.ts index d26295f7b11..620488a7416 100644 --- a/packages-exp/performance-exp/src/utils/string_merger.ts +++ b/packages-exp/performance-exp/src/utils/string_merger.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/test/setup.ts b/packages-exp/performance-exp/test/setup.ts index 1b4641c59a8..11f8e4cec16 100644 --- a/packages-exp/performance-exp/test/setup.ts +++ b/packages-exp/performance-exp/test/setup.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 0585b23752bfebb1649b5ea5e128d32f3b59b03a Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Tue, 15 Sep 2020 17:18:27 -0700 Subject: [PATCH 4/6] Updating the version to 0.0.800 on both the performance-exp and performance-types-exp packages --- packages-exp/performance-exp/package.json | 4 ++-- packages-exp/performance-types-exp/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages-exp/performance-exp/package.json b/packages-exp/performance-exp/package.json index 8bbc5df8a72..5b5f4d8358a 100644 --- a/packages-exp/performance-exp/package.json +++ b/packages-exp/performance-exp/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/performance-exp", - "version": "0.4.1", + "version": "0.0.800", "description": "Firebase performance for web", "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -29,7 +29,7 @@ "@firebase/logger": "0.2.6", "@firebase/installations": "0.4.17", "@firebase/util": "0.3.2", - "@firebase/performance-types-exp": "0.0.13", + "@firebase/performance-types-exp": "0.0.800", "@firebase/component": "0.1.19", "tslib": "^1.11.1" }, diff --git a/packages-exp/performance-types-exp/package.json b/packages-exp/performance-types-exp/package.json index 892aefaeb03..788000426cf 100644 --- a/packages-exp/performance-types-exp/package.json +++ b/packages-exp/performance-types-exp/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/performance-types-exp", - "version": "0.0.13", + "version": "0.0.800", "description": "@firebase/performance Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 2bbb8b9031360bd1b86fbdc9f566c39b010facd5 Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Tue, 15 Sep 2020 17:22:09 -0700 Subject: [PATCH 5/6] Removing a file I added by mistake --- packages-exp/performance-exp/src/index.ts | 73 ----------------------- 1 file changed, 73 deletions(-) delete mode 100644 packages-exp/performance-exp/src/index.ts diff --git a/packages-exp/performance-exp/src/index.ts b/packages-exp/performance-exp/src/index.ts deleted file mode 100644 index 238b76118ee..00000000000 --- a/packages-exp/performance-exp/src/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * 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 { FirebaseApp } from '@firebase/app-types-exp'; -import { FirebasePerformance } from '@firebase/performance-types-exp'; -import { ERROR_FACTORY, ErrorCode } from './utils/errors'; -import { setupApi } from './services/api_service'; -import { PerformanceController } from './controllers/perf'; -import { - _registerComponent, - _getProvider, - registerVersion -} from '@firebase/app-exp'; -import { - InstanceFactory, - ComponentContainer, - Component, - ComponentType -} from '@firebase/component'; -import { SettingsService } from './services/settings_service'; -import { name, version } from '../package.json'; - -const DEFAULT_ENTRY_NAME = '[DEFAULT]'; - -export function getPerformance(app: FirebaseApp): FirebasePerformance { - const provider = _getProvider(app, 'performance-exp'); - const perfInstance = provider.getImmediate() as PerformanceController; - return perfInstance; -} - -const factory: InstanceFactory<'performance-exp'> = ( - container: ComponentContainer -) => { - // Dependencies - const app = container.getProvider('app-exp').getImmediate(); - const installations = container - .getProvider('installations-exp-internal') - .getImmediate(); - - if (app.name !== DEFAULT_ENTRY_NAME) { - throw ERROR_FACTORY.create(ErrorCode.FB_NOT_DEFAULT); - } - if (typeof window === 'undefined') { - throw ERROR_FACTORY.create(ErrorCode.NO_WINDOW); - } - setupApi(window); - SettingsService.getInstance().firebaseAppInstance = app; - SettingsService.getInstance().installationsService = installations; - return new PerformanceController(app); -}; - -export function registerPerformance(): void { - _registerComponent( - new Component('performance-exp', factory, ComponentType.PUBLIC) - ); -} - -registerPerformance(); -registerVersion(name, version); From 64359fbb404e57c058a5a66eddda428642360690 Mon Sep 17 00:00:00 2001 From: Julio Perez-Osuna Morales Date: Tue, 15 Sep 2020 17:47:23 -0700 Subject: [PATCH 6/6] Updating the copyright year of a few more files that I missed --- packages-exp/performance-exp/karma.conf.js | 2 +- packages-exp/performance-exp/rollup.config.js | 2 +- packages-exp/performance-exp/src/constants.ts | 2 +- packages-exp/performance-types-exp/index.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages-exp/performance-exp/karma.conf.js b/packages-exp/performance-exp/karma.conf.js index 7d6d24d6c2b..7f333fdb2fd 100644 --- a/packages-exp/performance-exp/karma.conf.js +++ b/packages-exp/performance-exp/karma.conf.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/rollup.config.js b/packages-exp/performance-exp/rollup.config.js index 9cfe6294e74..5696a0414f6 100644 --- a/packages-exp/performance-exp/rollup.config.js +++ b/packages-exp/performance-exp/rollup.config.js @@ -1,6 +1,6 @@ /** * @license - * Copyright 2018 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-exp/src/constants.ts b/packages-exp/performance-exp/src/constants.ts index b0566a2a8be..2cac126da97 100644 --- a/packages-exp/performance-exp/src/constants.ts +++ b/packages-exp/performance-exp/src/constants.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages-exp/performance-types-exp/index.d.ts b/packages-exp/performance-types-exp/index.d.ts index 55463ee8434..3f42d3c15d5 100644 --- a/packages-exp/performance-types-exp/index.d.ts +++ b/packages-exp/performance-types-exp/index.d.ts @@ -1,6 +1,6 @@ /** * @license - * Copyright 2017 Google LLC + * Copyright 2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License.