Skip to content

Commit 4473256

Browse files
cabljactaeold
andauthored
fix(analytics): validate against incomplete/invalid app_remove events (#1738)
* fix(analytics): validate against incomplete/invalid app_remove events * refactor(analytics): clean up filtering code * fix(analytics): check for value field * feat: Refactor analytics event handler and update CHANGELOG * reformat changelog. * nit: remove unncessary comment. * reword changelog. --------- Co-authored-by: Daniel Lee <[email protected]>
1 parent bc7c89a commit 4473256

File tree

3 files changed

+127
-6
lines changed

3 files changed

+127
-6
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
- Add `defineJsonSecret` API for storing structured JSON objects in Cloud Secret Manager
1+
- Add `defineJsonSecret` API for storing structured JSON objects in Cloud Secret Manager. (#1745)
2+
- Enhance validation against incomplete/invalid app_remove events to avoid runtime crashes. (#1738)

spec/v1/providers/analytics.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,9 @@ describe("Analytics Functions", () => {
207207
firstOpenTimestampMicros: "577978620000000",
208208
userProperties: {
209209
foo: {
210+
value: {
211+
stringValue: "bar",
212+
},
210213
setTimestampUsec: "514820220000000",
211214
},
212215
},
@@ -236,6 +239,7 @@ describe("Analytics Functions", () => {
236239
firstOpenTime: "1988-04-25T13:37:00.000Z",
237240
userProperties: {
238241
foo: {
242+
value: "bar",
239243
setTime: "1986-04-25T13:37:00.000Z",
240244
},
241245
},
@@ -301,6 +305,92 @@ describe("Analytics Functions", () => {
301305
analyticsSpecInput.data
302306
);
303307
});
308+
309+
it("should handle null and missing user property values without throwing", () => {
310+
const cloudFunction = analytics
311+
.event("app_remove")
312+
.onLog((data: analytics.AnalyticsEvent) => data);
313+
314+
const event: Event = {
315+
data: {
316+
eventDim: [
317+
{
318+
name: "app_remove",
319+
params: {},
320+
date: "20240114",
321+
timestampMicros: "1705257600000000",
322+
},
323+
],
324+
userDim: {
325+
userProperties: {
326+
// Invalid properties that should be filtered out:
327+
null_property: null,
328+
value_null: {
329+
value: null,
330+
},
331+
value_undefined: {
332+
value: undefined,
333+
},
334+
empty_object: {},
335+
value_empty_object: {
336+
value: {},
337+
},
338+
// Valid properties that should be kept:
339+
valid_string: {
340+
value: {
341+
stringValue: "test",
342+
},
343+
setTimestampUsec: "1486076786090987",
344+
},
345+
valid_empty_string: {
346+
value: {
347+
stringValue: "",
348+
},
349+
setTimestampUsec: "1486076786090987",
350+
},
351+
valid_zero: {
352+
value: {
353+
intValue: "0",
354+
},
355+
setTimestampUsec: "1486076786090987",
356+
},
357+
},
358+
},
359+
},
360+
context: {
361+
eventId: "70172329041928",
362+
timestamp: "2018-04-09T07:56:12.975Z",
363+
eventType: "providers/google.firebase.analytics/eventTypes/event.log",
364+
resource: {
365+
service: "app-measurement.com",
366+
name: "projects/project1/events/app_remove",
367+
},
368+
},
369+
};
370+
371+
return expect(cloudFunction(event.data, event.context)).to.eventually.deep.equal({
372+
reportingDate: "20240114",
373+
name: "app_remove",
374+
params: {},
375+
logTime: "2024-01-14T18:40:00.000Z",
376+
user: {
377+
userProperties: {
378+
valid_string: {
379+
value: "test",
380+
setTime: "2017-02-02T23:06:26.090Z",
381+
},
382+
valid_empty_string: {
383+
value: "",
384+
setTime: "2017-02-02T23:06:26.090Z",
385+
},
386+
valid_zero: {
387+
value: "0",
388+
setTime: "2017-02-02T23:06:26.090Z",
389+
},
390+
},
391+
},
392+
});
393+
});
304394
});
305395
});
306396

src/v1/providers/analytics.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,23 @@ export class AnalyticsEvent {
135135
}
136136
}
137137

138+
function isValidUserProperty(property: unknown): property is { value: unknown } {
139+
if (property == null || typeof property !== "object" || !("value" in property)) {
140+
return false;
141+
}
142+
143+
const { value } = property;
144+
if (value == null) {
145+
return false;
146+
}
147+
148+
if (typeof value === "object" && Object.keys(value).length === 0) {
149+
return false;
150+
}
151+
152+
return true;
153+
}
154+
138155
/**
139156
* Interface representing the user who triggered the events.
140157
*/
@@ -179,8 +196,10 @@ export class UserDimensions {
179196
// The following fields do need transformations of some sort.
180197
copyTimestampToString(wireFormat, this, "firstOpenTimestampMicros", "firstOpenTime");
181198
this.userProperties = {}; // With no entries in the wire format, present an empty (as opposed to absent) map.
182-
copyField(wireFormat, this, "userProperties", (r) => {
183-
const entries = Object.entries(r).map(([k, v]) => [k, new UserPropertyValue(v)]);
199+
copyField(wireFormat, this, "userProperties", (r: unknown) => {
200+
const entries = Object.entries(r as Record<string, unknown>)
201+
.filter(([, v]) => isValidUserProperty(v))
202+
.map(([k, v]) => [k, new UserPropertyValue(v)]);
184203
return Object.fromEntries(entries);
185204
});
186205
copyField(wireFormat, this, "bundleInfo", (r) => new ExportBundleInfo(r) as any);
@@ -454,9 +473,20 @@ function mapKeys<Obj, Transform extends (key: keyof Obj) => any>(
454473
// method always returns a string, similarly to avoid loss of precision, unlike the less-conservative
455474
// 'unwrapValue' method just below.
456475
/** @hidden */
457-
function unwrapValueAsString(wrapped: any): string {
458-
const key: string = Object.keys(wrapped)[0];
459-
return wrapped[key].toString();
476+
function unwrapValueAsString(wrapped: unknown): string {
477+
if (!wrapped || typeof wrapped !== "object") {
478+
return "";
479+
}
480+
const keys = Object.keys(wrapped);
481+
if (keys.length === 0) {
482+
return "";
483+
}
484+
const key: string = keys[0];
485+
const value = (wrapped as Record<string, unknown>)[key];
486+
if (value === null || value === undefined) {
487+
return "";
488+
}
489+
return value.toString();
460490
}
461491

462492
// Ditto as the method above, but returning the values in the idiomatic JavaScript type (string for strings,

0 commit comments

Comments
 (0)