Skip to content

Commit 3435ba9

Browse files
authored
Used Trusted Types When API is available for gtag URL creation (#7155)
* Move changes from pr#7052 to fresh branch * Move @types/trusted-types into dev deps
1 parent 0e1f12c commit 3435ba9

File tree

6 files changed

+138
-5
lines changed

6 files changed

+138
-5
lines changed

.changeset/wet-cooks-doubt.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@firebase/analytics': patch
3+
---
4+
5+
Use the Trusted Types API when composing the gtag URL.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"@types/sinon": "9.0.11",
8686
"@types/sinon-chai": "3.2.9",
8787
"@types/tmp": "0.2.3",
88+
"@types/trusted-types": "2.0.3",
8889
"@types/yargs": "17.0.22",
8990
"@typescript-eslint/eslint-plugin": "5.43.0",
9091
"@typescript-eslint/eslint-plugin-tslint": "5.43.0",

packages/analytics/src/errors.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const enum AnalyticsError {
2727
FETCH_THROTTLE = 'fetch-throttle',
2828
CONFIG_FETCH_FAILED = 'config-fetch-failed',
2929
NO_API_KEY = 'no-api-key',
30-
NO_APP_ID = 'no-app-id'
30+
NO_APP_ID = 'no-app-id',
31+
INVALID_GTAG_RESOURCE = 'invalid-gtag-resource'
3132
}
3233

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

7073
interface ErrorParams {
@@ -77,6 +80,7 @@ interface ErrorParams {
7780
};
7881
[AnalyticsError.INVALID_ANALYTICS_CONTEXT]: { errorInfo: string };
7982
[AnalyticsError.INDEXEDDB_UNAVAILABLE]: { errorInfo: string };
83+
[AnalyticsError.INVALID_GTAG_RESOURCE]: { gtagURL: string };
8084
}
8185

8286
export const ERROR_FACTORY = new ErrorFactory<AnalyticsError, ErrorParams>(

packages/analytics/src/helpers.test.ts

+70-2
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ import {
2424
insertScriptTag,
2525
wrapOrCreateGtag,
2626
findGtagScriptOnPage,
27-
promiseAllSettled
27+
promiseAllSettled,
28+
createGtagTrustedTypesScriptURL,
29+
createTrustedTypesPolicy
2830
} from './helpers';
29-
import { GtagCommand } from './constants';
31+
import { GtagCommand, GTAG_URL } from './constants';
3032
import { Deferred } from '@firebase/util';
3133
import { ConsentSettings } from './public-types';
3234
import { removeGtagScripts } from '../testing/gtag-script-util';
35+
import { logger } from './logger';
36+
import { AnalyticsError, ERROR_FACTORY } from './errors';
3337

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

53+
describe('Trusted Types policies and functions', () => {
54+
describe('Trusted types exists', () => {
55+
let ttStub: SinonStub;
56+
57+
beforeEach(() => {
58+
ttStub = stub(
59+
window.trustedTypes as TrustedTypePolicyFactory,
60+
'createPolicy'
61+
).returns({
62+
createScriptURL: (s: string) => s
63+
} as any);
64+
});
65+
66+
afterEach(() => {
67+
removeGtagScripts();
68+
ttStub.restore();
69+
});
70+
71+
it('Verify trustedTypes is called if the API is available', () => {
72+
const trustedTypesPolicy = createTrustedTypesPolicy(
73+
'firebase-js-sdk-policy',
74+
{
75+
createScriptURL: createGtagTrustedTypesScriptURL
76+
}
77+
);
78+
79+
expect(ttStub).to.be.called;
80+
expect(trustedTypesPolicy).not.to.be.undefined;
81+
});
82+
83+
it('createGtagTrustedTypesScriptURL verifies gtag URL base exists when a URL is provided', () => {
84+
expect(createGtagTrustedTypesScriptURL(GTAG_URL)).to.equal(GTAG_URL);
85+
});
86+
87+
it('createGtagTrustedTypesScriptURL rejects URLs with non-gtag base', () => {
88+
const NON_GTAG_URL = 'http://iamnotgtag.com';
89+
const loggerWarnStub = stub(logger, 'warn');
90+
const errorMessage = ERROR_FACTORY.create(
91+
AnalyticsError.INVALID_GTAG_RESOURCE,
92+
{
93+
gtagURL: NON_GTAG_URL
94+
}
95+
).message;
96+
97+
expect(createGtagTrustedTypesScriptURL(NON_GTAG_URL)).to.equal('');
98+
expect(loggerWarnStub).to.be.calledWith(errorMessage);
99+
});
100+
});
101+
102+
describe('Trusted types does not exist', () => {
103+
it('Verify trustedTypes functions are not called if the API is not available', () => {
104+
delete window.trustedTypes;
105+
const trustedTypesPolicy = createTrustedTypesPolicy(
106+
'firebase-js-sdk-policy',
107+
{
108+
createScriptURL: createGtagTrustedTypesScriptURL
109+
}
110+
);
111+
112+
expect(trustedTypesPolicy).to.be.undefined;
113+
});
114+
});
115+
});
116+
49117
describe('Gtag wrapping functions', () => {
50118
afterEach(() => {
51119
removeGtagScripts();

packages/analytics/src/helpers.ts

+51-1
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,25 @@ import {
2424
import { DynamicConfig, DataLayer, Gtag, MinimalDynamicConfig } from './types';
2525
import { GtagCommand, GTAG_URL } from './constants';
2626
import { logger } from './logger';
27+
import { AnalyticsError, ERROR_FACTORY } from './errors';
2728

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

32+
/**
33+
* Verifies and creates a TrustedScriptURL.
34+
*/
35+
export function createGtagTrustedTypesScriptURL(url: string): string {
36+
if (!url.startsWith(GTAG_URL)) {
37+
const err = ERROR_FACTORY.create(AnalyticsError.INVALID_GTAG_RESOURCE, {
38+
gtagURL: url
39+
});
40+
logger.warn(err.message);
41+
return '';
42+
}
43+
return url;
44+
}
45+
3146
/**
3247
* Makeshift polyfill for Promise.allSettled(). Resolves when all promises
3348
* have either resolved or rejected.
@@ -40,6 +55,29 @@ export function promiseAllSettled<T>(
4055
return Promise.all(promises.map(promise => promise.catch(e => e)));
4156
}
4257

58+
/**
59+
* Creates a TrustedTypePolicy object that implements the rules passed as policyOptions.
60+
*
61+
* @param policyName A string containing the name of the policy
62+
* @param policyOptions Object containing implementations of instance methods for TrustedTypesPolicy, see {@link https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy#instance_methods
63+
* | the TrustedTypePolicy reference documentation}.
64+
*/
65+
export function createTrustedTypesPolicy(
66+
policyName: string,
67+
policyOptions: Partial<TrustedTypePolicyOptions>
68+
): Partial<TrustedTypePolicy> | undefined {
69+
// Create a TrustedTypes policy that we can use for updating src
70+
// properties
71+
let trustedTypesPolicy: Partial<TrustedTypePolicy> | undefined;
72+
if (window.trustedTypes) {
73+
trustedTypesPolicy = window.trustedTypes.createPolicy(
74+
policyName,
75+
policyOptions
76+
);
77+
}
78+
return trustedTypesPolicy;
79+
}
80+
4381
/**
4482
* Inserts gtag script tag into the page to asynchronously download gtag.
4583
* @param dataLayerName Name of datalayer (most often the default, "_dataLayer").
@@ -48,10 +86,22 @@ export function insertScriptTag(
4886
dataLayerName: string,
4987
measurementId: string
5088
): void {
89+
const trustedTypesPolicy = createTrustedTypesPolicy(
90+
'firebase-js-sdk-policy',
91+
{
92+
createScriptURL: createGtagTrustedTypesScriptURL
93+
}
94+
);
95+
5196
const script = document.createElement('script');
5297
// We are not providing an analyticsId in the URL because it would trigger a `page_view`
5398
// without fid. We will initialize ga-id using gtag (config) command together with fid.
54-
script.src = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;
99+
100+
const gtagScriptURL = `${GTAG_URL}?l=${dataLayerName}&id=${measurementId}`;
101+
(script.src as string | TrustedScriptURL) = trustedTypesPolicy
102+
? (trustedTypesPolicy as TrustedTypePolicy)?.createScriptURL(gtagScriptURL)
103+
: gtagScriptURL;
104+
55105
script.async = true;
56106
document.head.appendChild(script);
57107
}

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -3693,6 +3693,11 @@
36933693
resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz#8f80dd965ad81f3e1bc26d6f5c727e132721ff40"
36943694
integrity sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==
36953695

3696+
3697+
version "2.0.3"
3698+
resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz#a136f83b0758698df454e328759dbd3d44555311"
3699+
integrity sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==
3700+
36963701
"@types/vinyl@^2.0.4":
36973702
version "2.0.6"
36983703
resolved "https://registry.npmjs.org/@types/vinyl/-/vinyl-2.0.6.tgz#b2d134603557a7c3d2b5d3dc23863ea2b5eb29b0"

0 commit comments

Comments
 (0)