Skip to content

Commit 1778372

Browse files
committed
Add validation for custom metrics and custom attributes, and throw a Firebase Performance error when a specified metric, attribute name, or attribute value fails validation.
1 parent fc1f1bc commit 1778372

File tree

7 files changed

+197
-8
lines changed

7 files changed

+197
-8
lines changed

packages/performance/src/resources/trace.test.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,14 @@ describe('Firebase Performance > trace', () => {
116116

117117
expect(trace.getMetric('cacheHits')).to.eql(600);
118118
});
119+
120+
it('throws error if metric doesn\'t exist and has invalid name', () => {
121+
expect(() => trace.incrementMetric('_invalidMetric', 1)).to.throw();
122+
});
119123
});
120124

121125
describe('#putMetric', () => {
122-
it('creates new metric if one doesnt exist.', () => {
126+
it('creates new metric if one doesnt exist and has valid name.', () => {
123127
trace.putMetric('cacheHits', 200);
124128

125129
expect(trace.getMetric('cacheHits')).to.eql(200);
@@ -131,6 +135,10 @@ describe('Firebase Performance > trace', () => {
131135

132136
expect(trace.getMetric('cacheHits')).to.eql(400);
133137
});
138+
139+
it('throws error if metric doesn\'t exist and has invalid name', () => {
140+
expect(() => trace.putMetric('_invalidMetric', 1)).to.throw();
141+
});
134142
});
135143

136144
describe('#getMetric', () => {
@@ -172,6 +180,17 @@ describe('Firebase Performance > trace', () => {
172180

173181
expect(trace.getAttributes()).to.eql({ level: '7' });
174182
});
183+
184+
it('throws error if attribute name is invalid', () => {
185+
expect(() => trace.putAttribute('_invalidAttribute', '1')).to.throw();
186+
});
187+
188+
it('throws error if attribute value is invalid', () => {
189+
const longAttributeValue =
190+
'too-long-attribute-value-over-one-hundred-characters-too-long-attribute-value-over-one-' +
191+
'hundred-charac';
192+
expect(() => trace.putAttribute('validName', longAttributeValue)).to.throw();
193+
});
175194
});
176195

177196
describe('#getAttribute', () => {

packages/performance/src/resources/trace.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {
2727
import { Api } from '../services/api_service';
2828
import { logTrace } from '../services/perf_logger';
2929
import { ERROR_FACTORY, ErrorCode } from '../utils/errors';
30+
import { isValidCustomAttributeName, isValidCustomAttributeValue } from '../utils/attributes_utils';
31+
import { isValidCustomMetricName } from '../utils/metric_utils';
3032
import { PerformanceTrace } from '@firebase/performance-types';
3133

3234
const enum TraceState {
@@ -146,7 +148,7 @@ export class Trace implements PerformanceTrace {
146148
*/
147149
incrementMetric(counter: string, num = 1): void {
148150
if (this.counters[counter] === undefined) {
149-
this.counters[counter] = 0;
151+
this.putMetric(counter, 0);
150152
}
151153
this.counters[counter] += num;
152154
}
@@ -158,7 +160,13 @@ export class Trace implements PerformanceTrace {
158160
* @param num Set custom metric to this value
159161
*/
160162
putMetric(counter: string, num: number): void {
161-
this.counters[counter] = num;
163+
if (isValidCustomMetricName(counter)) {
164+
this.counters[counter] = num;
165+
} else {
166+
throw ERROR_FACTORY.create(ErrorCode.INVALID_CUSTOM_METRIC_NAME, {
167+
customMetricName: counter
168+
});
169+
}
162170
}
163171

164172
/**
@@ -176,7 +184,23 @@ export class Trace implements PerformanceTrace {
176184
* @param value
177185
*/
178186
putAttribute(attr: string, value: string): void {
179-
this.customAttributes[attr] = value;
187+
const isValidName = isValidCustomAttributeName(attr);
188+
const isValidValue = isValidCustomAttributeValue(value);
189+
if (isValidName && isValidValue) {
190+
this.customAttributes[attr] = value;
191+
return;
192+
}
193+
// Throw appropriate error when the attribute name or value is invalid.
194+
if (!isValidName){
195+
throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_NAME, {
196+
attributeName: attr
197+
});
198+
}
199+
if (!isValidValue){
200+
throw ERROR_FACTORY.create(ErrorCode.INVALID_ATTRIBUTE_VALUE, {
201+
attributeValue: value
202+
});
203+
}
180204
}
181205

182206
/**

packages/performance/src/utils/attribute_utils.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import {
2323
getVisibilityState,
2424
VisibilityState,
2525
getServiceWorkerStatus,
26-
getEffectiveConnectionType
26+
getEffectiveConnectionType,
27+
isValidCustomAttributeName,
28+
isValidCustomAttributeValue,
2729
} from './attributes_utils';
2830

2931
import '../../test/setup';
@@ -172,4 +174,47 @@ describe('Firebase Performance > attribute_utils', () => {
172174
expect(getEffectiveConnectionType()).to.be.eql(0);
173175
});
174176
});
175-
});
177+
178+
describe('#isValidCustomAttributeName', () => {
179+
afterEach(() => {
180+
restore();
181+
});
182+
183+
it('returns true when name is valid', () => {
184+
expect(isValidCustomAttributeName('validCustom_Attribute_Name')).to.be.true;
185+
});
186+
187+
it('returns false when name is too long', () => {
188+
expect(isValidCustomAttributeName('invalid_custom_name_over_forty_characters')).to.be.false;
189+
});
190+
191+
it('returns false when name starts with a reserved prefix', () => {
192+
expect(isValidCustomAttributeName('firebase_invalidCustomName')).to.be.false;
193+
});
194+
195+
it('returns false when name does not begin with a letter', () => {
196+
expect(isValidCustomAttributeName('_invalidCustomName')).to.be.false;
197+
});
198+
199+
it('returns false when name contains prohibited characters', () => {
200+
expect(isValidCustomAttributeName('invalidCustomName&')).to.be.false;
201+
});
202+
});
203+
204+
describe('#isValidCustomAttributeValue', () => {
205+
afterEach(() => {
206+
restore();
207+
});
208+
209+
it('returns true when value is valid', () => {
210+
expect(isValidCustomAttributeValue('valid_attribute_value')).to.be.true;
211+
});
212+
213+
it('returns false when value is too long', () => {
214+
const longAttributeValue =
215+
'too_long_attribute_value_over_one_hundred_characters_too_long_attribute_value_over_one_' +
216+
'hundred_charac';
217+
expect(isValidCustomAttributeValue(longAttributeValue)).to.be.false;
218+
});
219+
});
220+
});

packages/performance/src/utils/attributes_utils.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ const enum EffectiveConnectionType {
4141
CONNECTION_4G = 4
4242
}
4343

44+
const RESERVED_ATTRIBUTE_PREFIXES = ['firebase_', 'google_', 'ga_'];
45+
const ATTRIBUTE_FORMAT_REGEX = new RegExp('^[a-zA-Z]\\w*$');
46+
const MAX_ATTRIBUTE_NAME_LENGTH = 40;
47+
const MAX_ATTRIBUTE_VALUE_LENGTH = 100;
48+
4449
export function getServiceWorkerStatus(): ServiceWorkerStatus {
4550
const navigator = Api.getInstance().navigator;
4651
if ('serviceWorker' in navigator) {
@@ -88,3 +93,15 @@ export function getEffectiveConnectionType(): EffectiveConnectionType {
8893
return EffectiveConnectionType.UNKNOWN;
8994
}
9095
}
96+
97+
export function isValidCustomAttributeName(name: string): boolean {
98+
if (name.length > MAX_ATTRIBUTE_NAME_LENGTH) {
99+
return false;
100+
}
101+
const matchesReservedPrefix = RESERVED_ATTRIBUTE_PREFIXES.some(prefix => name.startsWith(prefix));
102+
return !matchesReservedPrefix && !!name.match(ATTRIBUTE_FORMAT_REGEX);
103+
}
104+
105+
export function isValidCustomAttributeValue(value: string): boolean {
106+
return value.length <= MAX_ATTRIBUTE_VALUE_LENGTH;
107+
}

packages/performance/src/utils/errors.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ export const enum ErrorCode {
2727
NO_API_KEY = 'no api key',
2828
INVALID_CC_LOG = 'invalid cc log',
2929
FB_NOT_DEFAULT = 'FB not default',
30-
RC_NOT_OK = 'RC response not ok'
30+
RC_NOT_OK = 'RC response not ok',
31+
INVALID_ATTRIBUTE_NAME = 'invalid attribute name',
32+
INVALID_ATTRIBUTE_VALUE = 'invalid attribute value',
33+
INVALID_CUSTOM_METRIC_NAME = 'invalide custom metric name',
3134
}
3235

3336
const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
@@ -40,12 +43,18 @@ const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = {
4043
[ErrorCode.INVALID_CC_LOG]: 'Attempted to queue invalid cc event',
4144
[ErrorCode.FB_NOT_DEFAULT]:
4245
'Performance can only start when Firebase app instance is the default one.',
43-
[ErrorCode.RC_NOT_OK]: 'RC response is not ok'
46+
[ErrorCode.RC_NOT_OK]: 'RC response is not ok',
47+
[ErrorCode.INVALID_ATTRIBUTE_NAME]: 'Attribute name {$attributeName} is invalid.',
48+
[ErrorCode.INVALID_ATTRIBUTE_VALUE]: 'Attribute value {$attributeValue} is invalid.',
49+
[ErrorCode.INVALID_CUSTOM_METRIC_NAME]: 'Custom metric name {$customMetricName} is invalid',
4450
};
4551

4652
interface ErrorParams {
4753
[ErrorCode.TRACE_STARTED_BEFORE]: { traceName: string };
4854
[ErrorCode.TRACE_STOPPED_BEFORE]: { traceName: string };
55+
[ErrorCode.INVALID_ATTRIBUTE_NAME]: { attributeName: string };
56+
[ErrorCode.INVALID_ATTRIBUTE_VALUE]: { attributeValue: string };
57+
[ErrorCode.INVALID_CUSTOM_METRIC_NAME]: { customMetricName: string };
4958
}
5059

5160
export const ERROR_FACTORY = new ErrorFactory<ErrorCode, ErrorParams>(
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF unknown KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { restore } from 'sinon';
19+
import { expect } from 'chai';
20+
21+
import {
22+
isValidCustomMetricName,
23+
} from './metric_utils';
24+
25+
import '../../test/setup';
26+
27+
describe('Firebase Performance > metric_utils', () => {
28+
29+
describe('#isValidCustomMetricName', () => {
30+
afterEach(() => {
31+
restore();
32+
});
33+
34+
it('returns true when name is valid', () => {
35+
expect(isValidCustomMetricName('validCustom_Metric_Name')).to.be.true;
36+
});
37+
38+
it('returns false when name is too long', () => {
39+
const longMetricName =
40+
'too_long_metric_name_over_one_hundred_characters_too_long_metric_name_over_one_' +
41+
'hundred_characters_too';
42+
expect(isValidCustomMetricName(longMetricName)).to.be.false;
43+
});
44+
45+
it('returns false when name starts with a reserved prefix', () => {
46+
expect(isValidCustomMetricName('_invalidMetricName')).to.be.false;
47+
});
48+
});
49+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright 2019 Google Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
const MAX_METRIC_NAME_LENGTH = 100;
19+
const RESERVED_AUTO_PREFIX = '_';
20+
21+
export function isValidCustomMetricName(name: string): boolean {
22+
if (name.length > MAX_METRIC_NAME_LENGTH) {
23+
return false;
24+
}
25+
return !name.startsWith(RESERVED_AUTO_PREFIX);
26+
}

0 commit comments

Comments
 (0)