voluptasmollitia
Version:
Monorepo for the Firebase JavaScript SDK
414 lines (380 loc) • 15.6 kB
text/typescript
/**
* @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(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('will not drop custom events sent before initialization finishes', async () => {
getIidStub.returns(IID);
stub(attributeUtils, 'getVisibilityState').returns(VISIBILITY_STATE);
stub(initializationService, 'isPerfInitialized').returns(false);
// Simulates logging being enabled after initialization completes.
const initializationPromise = Promise.resolve().then(() => {
SettingsService.getInstance().loggingEnabled = true;
SettingsService.getInstance().logTraceAfterSampling = true;
});
stub(initializationService, 'getInitializationPromise').returns(
initializationPromise
);
const trace = new Trace(TRACE_NAME);
trace.record(START_TIME, DURATION);
await initializationPromise.then(() => {
clock.tick(1);
});
expect(addToQueueStub).to.be.called;
});
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);
stub(initializationService, 'isPerfInitialized').returns(true);
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);
stub(initializationService, 'isPerfInitialized').returns(true);
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);
stub(initializationService, 'isPerfInitialized').returns(true);
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);
stub(initializationService, 'isPerfInitialized').returns(true);
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}}}`;
stub(initializationService, 'isPerfInitialized').returns(true);
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}}}`;
stub(initializationService, 'isPerfInitialized').returns(true);
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: () => {}
};
stub(initializationService, 'isPerfInitialized').returns(true);
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: () => {}
};
stub(initializationService, 'isPerfInitialized').returns(true);
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;
});
});
});