1+ /* This Source Code Form is subject to the terms of the Mozilla Public
2+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+ import type { CommonMetricData } from "../index.js" ;
6+ import { MetricType } from "../index.js" ;
7+ import { Context } from "../../context.js" ;
8+ import { Metric } from "../metric.js" ;
9+ import { isString , JSONValue , truncateStringAtBoundaryWithError } from "../../utils.js" ;
10+ import { ErrorType } from "../../error/error_type.js" ;
11+
12+ export const MAX_LIST_LENGTH = 20 ;
13+ export const MAX_STRING_LENGTH = 50 ;
14+
15+ export class StringListMetric extends Metric < string [ ] , string [ ] > {
16+ constructor ( v : unknown ) {
17+ super ( v ) ;
18+ }
19+
20+ validate ( v : unknown ) : v is string [ ] {
21+ if ( ! Array . isArray ( v ) ) {
22+ return false ;
23+ }
24+
25+ if ( v . length > MAX_LIST_LENGTH ) {
26+ return false ;
27+ }
28+ for ( const s of v ) {
29+ if ( ! isString ( s ) || s . length > MAX_STRING_LENGTH ) {
30+ return false ;
31+ }
32+ }
33+
34+ return true ;
35+ }
36+
37+ payload ( ) : string [ ] {
38+ return this . _inner ;
39+ }
40+ }
41+
42+ /**
43+ * A string list metric.
44+ *
45+ * This allows appending a string value with arbitrary content to a list.
46+ * The list is length-limited to `MAX_LIST_LENGTH`.
47+ * Strings are length-limited to `MAX_STRING_LENGTH` bytes.
48+ */
49+ class StringListMetricType extends MetricType {
50+ constructor ( meta : CommonMetricData ) {
51+ super ( "string_list" , meta ) ;
52+ }
53+
54+ /**
55+ * Sets to the specified string list value.
56+ *
57+ * # Note
58+ *
59+ * Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error.
60+ *
61+ * Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes
62+ * and logs an error.
63+ *
64+ * @param value the list of string to set the metric to.
65+ */
66+ set ( value : string [ ] ) : void {
67+ Context . dispatcher . launch ( async ( ) => {
68+ if ( ! this . shouldRecord ( Context . uploadEnabled ) ) {
69+ return ;
70+ }
71+ const truncatedList : string [ ] = [ ] ;
72+ if ( value . length > MAX_LIST_LENGTH ) {
73+ await Context . errorManager . record (
74+ this ,
75+ ErrorType . InvalidValue ,
76+ `String list length of ${ value . length } exceeds maximum of ${ MAX_LIST_LENGTH } .`
77+ ) ;
78+ }
79+
80+ for ( let i = 0 ; i < Math . min ( value . length , MAX_LIST_LENGTH ) ; ++ i ) {
81+ const truncatedString = await truncateStringAtBoundaryWithError ( this , value [ i ] , MAX_STRING_LENGTH ) ;
82+ truncatedList . push ( truncatedString )
83+ }
84+ const metric = new StringListMetric ( truncatedList ) ;
85+ await Context . metricsDatabase . record ( this , metric ) ;
86+ } ) ;
87+ }
88+
89+ /**
90+ * Adds a new string `value` to the list.
91+ *
92+ * # Note
93+ *
94+ * - If the list is already of length `MAX_LIST_LENGTH`, log an error.
95+ * - Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes
96+ * and logs an error.
97+ *
98+ * @param value The string to add.
99+ */
100+ add ( value : string ) : void {
101+ Context . dispatcher . launch ( async ( ) => {
102+ if ( ! this . shouldRecord ( Context . uploadEnabled ) ) {
103+ return ;
104+ }
105+
106+ const truncatedValue = await truncateStringAtBoundaryWithError ( this , value , MAX_STRING_LENGTH ) ;
107+
108+ const transformFn = ( ( value ) => {
109+ return ( v ?: JSONValue ) : StringListMetric => {
110+ let metric : StringListMetric ;
111+ let result : string [ ] ;
112+ try {
113+ metric = new StringListMetric ( v ) ;
114+ result = metric . get ( ) ;
115+ if ( result . length < MAX_LIST_LENGTH ) {
116+ result . push ( value ) ;
117+ } else {
118+ Context . errorManager . record (
119+ this ,
120+ ErrorType . InvalidValue ,
121+ `String list length of ${ result . length } +1 exceeds maximum of ${ MAX_LIST_LENGTH } .`
122+ ) ;
123+ }
124+ } catch {
125+ metric = new StringListMetric ( [ value ] ) ;
126+ result = [ value ] ;
127+ }
128+
129+ metric . set ( result ) ;
130+ return metric
131+ }
132+ } ) ( truncatedValue ) ;
133+
134+ await Context . metricsDatabase . transform ( this , transformFn ) ;
135+ } ) ;
136+ }
137+
138+ /**
139+ * Test-only API**
140+ *
141+ * Gets the currently stored value as a string array.
142+ *
143+ * This doesn't clear the stored value.
144+ *
145+ * TODO: Only allow this function to be called on test mode (depends on Bug 1682771).
146+ *
147+ * @param ping the ping from which we want to retrieve this metrics value from.
148+ * Defaults to the first value in `sendInPings`.
149+ * @returns The value found in storage or `undefined` if nothing was found.
150+ */
151+ async testGetValue ( ping : string = this . sendInPings [ 0 ] ) : Promise < string [ ] | undefined > {
152+ let metric : string [ ] | undefined ;
153+ await Context . dispatcher . testLaunch ( async ( ) => {
154+ metric = await Context . metricsDatabase . getMetric < string [ ] > ( ping , this ) ;
155+ } ) ;
156+ return metric ;
157+ }
158+ }
159+
160+ export default StringListMetricType ;
0 commit comments