UNPKG

9.87 kBJavaScriptView Raw
1"use strict";
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.
23Object.defineProperty(exports, "__esModule", { value: true });
24exports.ExportBundleInfo = exports.UserPropertyValue = exports.UserDimensions = exports.AnalyticsEvent = exports.AnalyticsEventBuilder = exports._eventWithOptions = exports.event = exports.service = exports.provider = void 0;
25const cloud_functions_1 = require("../cloud-functions");
26/** @internal */
27exports.provider = "google.analytics";
28/** @internal */
29exports.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 */
38function event(analyticsEventType) {
39 return _eventWithOptions(analyticsEventType, {});
40}
41exports.event = event;
42/** @internal */
43function _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}
51exports._eventWithOptions = _eventWithOptions;
52/**
53 * The Firebase Analytics event builder interface.
54 *
55 * Access via `functions.analytics.event()`.
56 */
57class 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}
87exports.AnalyticsEventBuilder = AnalyticsEventBuilder;
88/** Interface representing a Firebase Analytics event that was logged for a specific user. */
89class 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}
106exports.AnalyticsEvent = AnalyticsEvent;
107/**
108 * Interface representing the user who triggered the events.
109 */
110class 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}
131exports.UserDimensions = UserDimensions;
132/** Predefined or custom properties stored on the client side. */
133class UserPropertyValue {
134 /** @hidden */
135 constructor(wireFormat) {
136 copyField(wireFormat, this, "value", unwrapValueAsString);
137 copyTimestampToString(wireFormat, this, "setTimestampUsec", "setTime");
138 }
139}
140exports.UserPropertyValue = UserPropertyValue;
141/** Interface representing the bundle these events were uploaded to. */
142class ExportBundleInfo {
143 /** @hidden */
144 constructor(wireFormat) {
145 copyField(wireFormat, this, "bundleSequenceId");
146 copyTimestampToMillis(wireFormat, this, "serverTimestampOffsetMicros", "serverTimestampOffset");
147 }
148}
149exports.ExportBundleInfo = ExportBundleInfo;
150/** @hidden */
151function 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 */
162function copyField(from, to, field, transform = (from) => from) {
163 copyFieldTo(from, to, field, field, transform);
164}
165/** @hidden */
166function copyFields(from, to, fields) {
167 for (const field of fields) {
168 copyField(from, to, field);
169 }
170}
171function 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 */
205function 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 */
223const xValueNumberFields = ["intValue", "floatValue", "doubleValue"];
224/** @hidden */
225function 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 */
234function 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 */
243function copyTimestampToString(from, to, fromName, toName) {
244 if (from[fromName] !== undefined) {
245 to[toName] = new Date(from[fromName] / 1000).toISOString();
246 }
247}