1 | ;
|
2 | // The MIT License (MIT)
|
3 | //
|
4 | // Copyright (c) 2017 Firebase
|
5 | //
|
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
|
7 | // of this software and associated documentation files (the 'Software'), to deal
|
8 | // in the Software without restriction, including without limitation the rights
|
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10 | // copies of the Software, and to permit persons to whom the Software is
|
11 | // furnished to do so, subject to the following conditions:
|
12 | //
|
13 | // The above copyright notice and this permission notice shall be included in all
|
14 | // copies or substantial portions of the Software.
|
15 | //
|
16 | // THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
22 | // SOFTWARE.
|
23 | Object.defineProperty(exports, "__esModule", { value: true });
|
24 | exports.ExportBundleInfo = exports.UserPropertyValue = exports.UserDimensions = exports.AnalyticsEvent = exports.AnalyticsEventBuilder = exports._eventWithOptions = exports.event = exports.service = exports.provider = void 0;
|
25 | const cloud_functions_1 = require("../cloud-functions");
|
26 | /** @internal */
|
27 | exports.provider = "google.analytics";
|
28 | /** @internal */
|
29 | exports.service = "app-measurement.com";
|
30 | /**
|
31 | * Registers a function to handle analytics events.
|
32 | *
|
33 | * @param analyticsEventType Name of the analytics event type to which
|
34 | * this Cloud Function is scoped.
|
35 | *
|
36 | * @returns Analytics event builder interface.
|
37 | */
|
38 | function event(analyticsEventType) {
|
39 | return _eventWithOptions(analyticsEventType, {});
|
40 | }
|
41 | exports.event = event;
|
42 | /** @internal */
|
43 | function _eventWithOptions(analyticsEventType, options) {
|
44 | return new AnalyticsEventBuilder(() => {
|
45 | if (!process.env.GCLOUD_PROJECT) {
|
46 | throw new Error("process.env.GCLOUD_PROJECT is not set.");
|
47 | }
|
48 | return "projects/" + process.env.GCLOUD_PROJECT + "/events/" + analyticsEventType;
|
49 | }, options);
|
50 | }
|
51 | exports._eventWithOptions = _eventWithOptions;
|
52 | /**
|
53 | * The Firebase Analytics event builder interface.
|
54 | *
|
55 | * Access via `functions.analytics.event()`.
|
56 | */
|
57 | class AnalyticsEventBuilder {
|
58 | /** @hidden */
|
59 | constructor(triggerResource, options) {
|
60 | this.triggerResource = triggerResource;
|
61 | this.options = options;
|
62 | }
|
63 | /**
|
64 | * Event handler that fires every time a Firebase Analytics event occurs.
|
65 | *
|
66 | * @param handler Event handler that fires every time a Firebase Analytics event
|
67 | * occurs.
|
68 | *
|
69 | * @returns A function that you can export and deploy.
|
70 | */
|
71 | onLog(handler) {
|
72 | const dataConstructor = (raw) => {
|
73 | return new AnalyticsEvent(raw.data);
|
74 | };
|
75 | return (0, cloud_functions_1.makeCloudFunction)({
|
76 | handler,
|
77 | provider: exports.provider,
|
78 | eventType: "event.log",
|
79 | service: exports.service,
|
80 | legacyEventType: `providers/google.firebase.analytics/eventTypes/event.log`,
|
81 | triggerResource: this.triggerResource,
|
82 | dataConstructor,
|
83 | options: this.options,
|
84 | });
|
85 | }
|
86 | }
|
87 | exports.AnalyticsEventBuilder = AnalyticsEventBuilder;
|
88 | /** Interface representing a Firebase Analytics event that was logged for a specific user. */
|
89 | class AnalyticsEvent {
|
90 | /** @hidden */
|
91 | constructor(wireFormat) {
|
92 | this.params = {}; // In case of absent field, show empty (not absent) map.
|
93 | if (wireFormat.eventDim && wireFormat.eventDim.length > 0) {
|
94 | // If there's an eventDim, there'll always be exactly one.
|
95 | const eventDim = wireFormat.eventDim[0];
|
96 | copyField(eventDim, this, "name");
|
97 | copyField(eventDim, this, "params", (p) => mapKeys(p, unwrapValue));
|
98 | copyFieldTo(eventDim, this, "valueInUsd", "valueInUSD");
|
99 | copyFieldTo(eventDim, this, "date", "reportingDate");
|
100 | copyTimestampToString(eventDim, this, "timestampMicros", "logTime");
|
101 | copyTimestampToString(eventDim, this, "previousTimestampMicros", "previousLogTime");
|
102 | }
|
103 | copyFieldTo(wireFormat, this, "userDim", "user", (dim) => new UserDimensions(dim));
|
104 | }
|
105 | }
|
106 | exports.AnalyticsEvent = AnalyticsEvent;
|
107 | /**
|
108 | * Interface representing the user who triggered the events.
|
109 | */
|
110 | class UserDimensions {
|
111 | /** @hidden */
|
112 | constructor(wireFormat) {
|
113 | // These are interfaces or primitives, no transformation needed.
|
114 | copyFields(wireFormat, this, ["userId", "deviceInfo", "geoInfo", "appInfo"]);
|
115 | // The following fields do need transformations of some sort.
|
116 | copyTimestampToString(wireFormat, this, "firstOpenTimestampMicros", "firstOpenTime");
|
117 | this.userProperties = {}; // With no entries in the wire format, present an empty (as opposed to absent) map.
|
118 | copyField(wireFormat, this, "userProperties", (r) => {
|
119 | const entries = Object.entries(r).map(([k, v]) => [k, new UserPropertyValue(v)]);
|
120 | return Object.fromEntries(entries);
|
121 | });
|
122 | copyField(wireFormat, this, "bundleInfo", (r) => new ExportBundleInfo(r));
|
123 | // BUG(36000368) Remove when no longer necessary
|
124 | /* tslint:disable:no-string-literal */
|
125 | if (!this.userId && this.userProperties["user_id"]) {
|
126 | this.userId = this.userProperties["user_id"].value;
|
127 | }
|
128 | /* tslint:enable:no-string-literal */
|
129 | }
|
130 | }
|
131 | exports.UserDimensions = UserDimensions;
|
132 | /** Predefined or custom properties stored on the client side. */
|
133 | class UserPropertyValue {
|
134 | /** @hidden */
|
135 | constructor(wireFormat) {
|
136 | copyField(wireFormat, this, "value", unwrapValueAsString);
|
137 | copyTimestampToString(wireFormat, this, "setTimestampUsec", "setTime");
|
138 | }
|
139 | }
|
140 | exports.UserPropertyValue = UserPropertyValue;
|
141 | /** Interface representing the bundle these events were uploaded to. */
|
142 | class ExportBundleInfo {
|
143 | /** @hidden */
|
144 | constructor(wireFormat) {
|
145 | copyField(wireFormat, this, "bundleSequenceId");
|
146 | copyTimestampToMillis(wireFormat, this, "serverTimestampOffsetMicros", "serverTimestampOffset");
|
147 | }
|
148 | }
|
149 | exports.ExportBundleInfo = ExportBundleInfo;
|
150 | /** @hidden */
|
151 | function copyFieldTo(from, to, fromField, toField, transform) {
|
152 | if (typeof from[fromField] === "undefined") {
|
153 | return;
|
154 | }
|
155 | if (transform) {
|
156 | to[toField] = transform(from[fromField]);
|
157 | return;
|
158 | }
|
159 | to[toField] = from[fromField];
|
160 | }
|
161 | /** @hidden */
|
162 | function copyField(from, to, field, transform = (from) => from) {
|
163 | copyFieldTo(from, to, field, field, transform);
|
164 | }
|
165 | /** @hidden */
|
166 | function copyFields(from, to, fields) {
|
167 | for (const field of fields) {
|
168 | copyField(from, to, field);
|
169 | }
|
170 | }
|
171 | function mapKeys(obj, transform) {
|
172 | const entries = Object.entries(obj).map(([k, v]) => [k, transform(v)]);
|
173 | return Object.fromEntries(entries);
|
174 | }
|
175 | // The incoming payload will have fields like:
|
176 | // {
|
177 | // 'myInt': {
|
178 | // 'intValue': '123'
|
179 | // },
|
180 | // 'myDouble': {
|
181 | // 'doubleValue': 1.0
|
182 | // },
|
183 | // 'myFloat': {
|
184 | // 'floatValue': 1.1
|
185 | // },
|
186 | // 'myString': {
|
187 | // 'stringValue': 'hi!'
|
188 | // }
|
189 | // }
|
190 | //
|
191 | // The following method will remove these four types of 'xValue' fields, flattening them
|
192 | // to just their values, as a string:
|
193 | // {
|
194 | // 'myInt': '123',
|
195 | // 'myDouble': '1.0',
|
196 | // 'myFloat': '1.1',
|
197 | // 'myString': 'hi!'
|
198 | // }
|
199 | //
|
200 | // Note that while 'intValue' will have a quoted payload, 'doubleValue' and 'floatValue' will not. This
|
201 | // is due to the encoding library, which renders int64 values as strings to avoid loss of precision. This
|
202 | // method always returns a string, similarly to avoid loss of precision, unlike the less-conservative
|
203 | // 'unwrapValue' method just below.
|
204 | /** @hidden */
|
205 | function unwrapValueAsString(wrapped) {
|
206 | const key = Object.keys(wrapped)[0];
|
207 | return wrapped[key].toString();
|
208 | }
|
209 | // Ditto as the method above, but returning the values in the idiomatic JavaScript type (string for strings,
|
210 | // number for numbers):
|
211 | // {
|
212 | // 'myInt': 123,
|
213 | // 'myDouble': 1.0,
|
214 | // 'myFloat': 1.1,
|
215 | // 'myString': 'hi!'
|
216 | // }
|
217 | //
|
218 | // The field names in the incoming xValue fields identify the type a value has, which for JavaScript's
|
219 | // purposes can be divided into 'number' versus 'string'. This method will render all the numbers as
|
220 | // JavaScript's 'number' type, since we prefer using idiomatic types. Note that this may lead to loss
|
221 | // in precision for int64 fields, so use with care.
|
222 | /** @hidden */
|
223 | const xValueNumberFields = ["intValue", "floatValue", "doubleValue"];
|
224 | /** @hidden */
|
225 | function unwrapValue(wrapped) {
|
226 | const key = Object.keys(wrapped)[0];
|
227 | const value = unwrapValueAsString(wrapped);
|
228 | return xValueNumberFields.includes(key) ? Number(value) : value;
|
229 | }
|
230 | // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds.
|
231 | // The JavaScript convention is to use numbers denoted in milliseconds. This method
|
232 | // makes it easy to convert a field of one type into the other.
|
233 | /** @hidden */
|
234 | function copyTimestampToMillis(from, to, fromName, toName) {
|
235 | if (from[fromName] !== undefined) {
|
236 | to[toName] = Math.round(from[fromName] / 1000);
|
237 | }
|
238 | }
|
239 | // The JSON payload delivers timestamp fields as strings of timestamps denoted in microseconds.
|
240 | // In our SDK, we'd like to present timestamp as ISO-format strings. This method makes it easy
|
241 | // to convert a field of one type into the other.
|
242 | /** @hidden */
|
243 | function copyTimestampToString(from, to, fromName, toName) {
|
244 | if (from[fromName] !== undefined) {
|
245 | to[toName] = new Date(from[fromName] / 1000).toISOString();
|
246 | }
|
247 | }
|