Skip to content
Draft
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
86 changes: 86 additions & 0 deletions spec/v1/providers/analytics.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,92 @@ describe("Analytics Functions", () => {
analyticsSpecInput.data
);
});

it("should handle null and missing user property values without throwing", () => {
const cloudFunction = analytics
.event("app_remove")
.onLog((data: analytics.AnalyticsEvent) => data);

const event: Event = {
data: {
eventDim: [
{
name: "app_remove",
params: {},
date: "20240114",
timestampMicros: "1705257600000000",
},
],
userDim: {
userProperties: {
// Invalid properties that should be filtered out:
null_property: null,
value_null: {
value: null,
},
value_undefined: {
value: undefined,
},
empty_object: {},
value_empty_object: {
value: {},
},
// Valid properties that should be kept:
valid_string: {
value: {
stringValue: "test",
},
setTimestampUsec: "1486076786090987",
},
valid_empty_string: {
value: {
stringValue: "",
},
setTimestampUsec: "1486076786090987",
},
valid_zero: {
value: {
intValue: "0",
},
setTimestampUsec: "1486076786090987",
},
},
},
},
context: {
eventId: "70172329041928",
timestamp: "2018-04-09T07:56:12.975Z",
eventType: "providers/google.firebase.analytics/eventTypes/event.log",
resource: {
service: "app-measurement.com",
name: "projects/project1/events/app_remove",
},
},
};

return expect(cloudFunction(event.data, event.context)).to.eventually.deep.equal({
reportingDate: "20240114",
name: "app_remove",
params: {},
logTime: "2024-01-14T18:40:00.000Z",
user: {
userProperties: {
valid_string: {
value: "test",
setTime: "2017-02-02T23:06:26.090Z",
},
valid_empty_string: {
value: "",
setTime: "2017-02-02T23:06:26.090Z",
},
valid_zero: {
value: "0",
setTime: "2017-02-02T23:06:26.090Z",
},
},
},
});
});
});
});

Expand Down
47 changes: 42 additions & 5 deletions src/v1/providers/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,34 @@ export class UserDimensions {
// The following fields do need transformations of some sort.
copyTimestampToString(wireFormat, this, "firstOpenTimestampMicros", "firstOpenTime");
this.userProperties = {}; // With no entries in the wire format, present an empty (as opposed to absent) map.
copyField(wireFormat, this, "userProperties", (r) => {
const entries = Object.entries(r).map(([k, v]) => [k, new UserPropertyValue(v)]);
copyField(wireFormat, this, "userProperties", (r: unknown) => {
const entries = Object.entries(r as Record<string, unknown>)
.filter(([, v]) => {
if (v === null || v === undefined) {
return false;
}
if (typeof v !== "object") {
return false;
}
const vObj = v as Record<string, unknown>;
// Filter out empty objects
if (Object.keys(vObj).length === 0) {
return false;
}
// Filter if 'value' property exists but is null/undefined/empty object
if ("value" in vObj) {
const value = vObj.value;
if (
value === null ||
value === undefined ||
(typeof value === "object" && value !== null && Object.keys(value).length === 0)
) {
return false;
}
}
return true;
})
.map(([k, v]) => [k, new UserPropertyValue(v)]);
return Object.fromEntries(entries);
});
copyField(wireFormat, this, "bundleInfo", (r) => new ExportBundleInfo(r) as any);
Expand Down Expand Up @@ -454,9 +480,20 @@ function mapKeys<Obj, Transform extends (key: keyof Obj) => any>(
// method always returns a string, similarly to avoid loss of precision, unlike the less-conservative
// 'unwrapValue' method just below.
/** @hidden */
function unwrapValueAsString(wrapped: any): string {
const key: string = Object.keys(wrapped)[0];
return wrapped[key].toString();
function unwrapValueAsString(wrapped: unknown): string {
if (!wrapped || typeof wrapped !== "object") {
return "";
}
const keys = Object.keys(wrapped);
if (keys.length === 0) {
return "";
}
const key: string = keys[0];
const value = (wrapped as Record<string, unknown>)[key];
if (value === null || value === undefined) {
return "";
}
return value.toString();
}

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