Skip to content

Used Trusted Types When API is available for gtag URL creation #7155

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-cooks-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@firebase/analytics': patch
---

Use the Trusted Types API when composing the gtag URL.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"@types/sinon": "9.0.11",
"@types/sinon-chai": "3.2.9",
"@types/tmp": "0.2.3",
"@types/trusted-types": "2.0.3",
"@types/yargs": "17.0.22",
"@typescript-eslint/eslint-plugin": "5.43.0",
"@typescript-eslint/eslint-plugin-tslint": "5.43.0",
Expand Down
8 changes: 6 additions & 2 deletions packages/analytics/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const enum AnalyticsError {
FETCH_THROTTLE = 'fetch-throttle',
CONFIG_FETCH_FAILED = 'config-fetch-failed',
NO_API_KEY = 'no-api-key',
NO_APP_ID = 'no-app-id'
NO_APP_ID = 'no-app-id',
INVALID_GTAG_RESOURCE = 'invalid-gtag-resource'
}

const ERRORS: ErrorMap<AnalyticsError> = {
Expand Down Expand Up @@ -64,7 +65,9 @@ const ERRORS: ErrorMap<AnalyticsError> = {
'contain a valid API key.',
[AnalyticsError.NO_APP_ID]:
'The "appId" field is empty in the local Firebase config. Firebase Analytics requires this field to' +
'contain a valid app ID.'
'contain a valid app ID.',
[AnalyticsError.INVALID_GTAG_RESOURCE]:
'Trusted Types detected an invalid gtag resource: {$gtagURL}.'
};

interface ErrorParams {
Expand All @@ -77,6 +80,7 @@ interface ErrorParams {
};
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]: { errorInfo: string };
[AnalyticsError.INDEXEDDB_UNAVAILABLE]: { errorInfo: string };
[AnalyticsError.INVALID_GTAG_RESOURCE]: { gtagURL: string };
}

export const ERROR_FACTORY = new ErrorFactory<AnalyticsError, ErrorParams>(
Expand Down
72 changes: 70 additions & 2 deletions packages/analytics/src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ import {
insertScriptTag,
wrapOrCreateGtag,
findGtagScriptOnPage,
promiseAllSettled
promiseAllSettled,
createGtagTrustedTypesScriptURL,
createTrustedTypesPolicy
} from './helpers';
import { GtagCommand } from './constants';
import { GtagCommand, GTAG_URL } from './constants';
import { Deferred } from '@firebase/util';
import { ConsentSettings } from './public-types';
import { removeGtagScripts } from '../testing/gtag-script-util';
import { logger } from './logger';
import { AnalyticsError, ERROR_FACTORY } from './errors';

const fakeMeasurementId = 'abcd-efgh-ijkl';
const fakeAppId = 'my-test-app-1234';
Expand All @@ -46,6 +50,70 @@ const fakeDynamicConfig: DynamicConfig = {
};
const fakeDynamicConfigPromises = [Promise.resolve(fakeDynamicConfig)];

describe('Trusted Types policies and functions', () => {
describe('Trusted types exists', () => {
let ttStub: SinonStub;

beforeEach(() => {
ttStub = stub(
window.trustedTypes as TrustedTypePolicyFactory,
'createPolicy'
).returns({
createScriptURL: (s: string) => s
} as any);
});

afterEach(() => {
removeGtagScripts();
ttStub.restore();
});

it('Verify trustedTypes is called if the API is available', () => {
const trustedTypesPolicy = createTrustedTypesPolicy(
'firebase-js-sdk-policy',
{
createScriptURL: createGtagTrustedTypesScriptURL
}
);

expect(ttStub).to.be.called;
expect(trustedTypesPolicy).not.to.be.undefined;
});

it('createGtagTrustedTypesScriptURL verifies gtag URL base exists when a URL is provided', () => {
expect(createGtagTrustedTypesScriptURL(GTAG_URL)).to.equal(GTAG_URL);
});

it('createGtagTrustedTypesScriptURL rejects URLs with non-gtag base', () => {
const NON_GTAG_URL = 'http://iamnotgtag.com';
const loggerWarnStub = stub(logger, 'warn');
const errorMessage = ERROR_FACTORY.create(
AnalyticsError.INVALID_GTAG_RESOURCE,
{
gtagURL: NON_GTAG_URL
}
).message;

expect(createGtagTrustedTypesScriptURL(NON_GTAG_URL)).to.equal('');
expect(loggerWarnStub).to.be.calledWith(errorMessage);
});
});

describe('Trusted types does not exist', () => {
it('Verify trustedTypes functions are not called if the API is not available', () => {
delete window.trustedTypes;
const trustedTypesPolicy = createTrustedTypesPolicy(
'firebase-js-sdk-policy',
{
createScriptURL: createGtagTrustedTypesScriptURL
}
);

expect(trustedTypesPolicy).to.be.undefined;
});
});
});

describe('Gtag wrapping functions', () => {
afterEach(() => {
removeGtagScripts();
Expand Down
52 changes: 51 additions & 1 deletion packages/analytics/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,25 @@ import {
import { DynamicConfig, DataLayer, Gtag, MinimalDynamicConfig } from './types';
import { GtagCommand, GTAG_URL } from './constants';
import { logger } from './logger';
import { AnalyticsError, ERROR_FACTORY } from './errors';

// Possible parameter types for gtag 'event' and 'config' commands
type GtagConfigOrEventParams = ControlParams & EventParams & CustomParams;

/**
* Verifies and creates a TrustedScriptURL.
*/
export function createGtagTrustedTypesScriptURL(url: string): string {
if (!url.startsWith(GTAG_URL)) {
const err = ERROR_FACTORY.create(AnalyticsError.INVALID_GTAG_RESOURCE, {
gtagURL: url
});
logger.warn(err.message);
return '';
}
return url;
}

/**
* Makeshift polyfill for Promise.allSettled(). Resolves when all promises
* have either resolved or rejected.
Expand All @@ -40,6 +55,29 @@ export function promiseAllSettled<T>(
return Promise.all(promises.map(promise => promise.catch(e => e)));
}

/**
* Creates a TrustedTypePolicy object that implements the rules passed as policyOptions.
*
* @param policyName A string containing the name of the policy
* @param policyOptions Object containing implementations of instance methods for TrustedTypesPolicy, see {@link https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy#instance_methods
* | the TrustedTypePolicy reference documentation}.
*/
export function createTrustedTypesPolicy(
policyName: string,
policyOptions: Partial<TrustedTypePolicyOptions>
): Partial<TrustedTypePolicy> | undefined {
// Create a TrustedTypes policy that we can use for updating src
// properties
let trustedTypesPolicy: Partial<TrustedTypePolicy> | undefined;
if (window.trustedTypes) {
trustedTypesPolicy = window.trustedTypes.createPolicy(
policyName,
policyOptions
);
}
return trustedTypesPolicy;
}

/**
* Inserts gtag script tag into the page to asynchronously download gtag.
* @param dataLayerName Name of datalayer (most often the default, "_dataLayer").
Expand All @@ -48,10 +86,22 @@ export function insertScriptTag(
dataLayerName: string,
measurementId: string
): void {
const trustedTypesPolicy = createTrustedTypesPolicy(
'firebase-js-sdk-policy',
{
createScriptURL: createGtagTrustedTypesScriptURL
}
);

const script = document.createElement('script');
// We are not providing an analyticsId in the URL because it would trigger a `page_view`
// without fid. We will initialize ga-id using gtag (config) command together with fid.
script.src = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;

const gtagScriptURL = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;
(script.src as string | TrustedScriptURL) = trustedTypesPolicy
? (trustedTypesPolicy as TrustedTypePolicy)?.createScriptURL(gtagScriptURL)
: gtagScriptURL;

script.async = true;
document.head.appendChild(script);
}
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3693,6 +3693,11 @@
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40"
integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==

"@types/[email protected]":
version "2.0.3"
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==

"@types/vinyl@^2.0.4":
version "2.0.6"
resolved "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz#b2d134603557a7c3d2b5d3dc23863ea2b5eb29b0"
Expand Down