Skip to content

Commit c41f7d4

Browse files
authored
Error handling (#4)
* remove logging d'oh. * reimplement credential validation previously, we validated credentials by using /import ... this was not an effectively strategy for mp => sheets as consumer permissions can still view reports. now auth checks should give clear error messages if the credentials are bad. * standardize tracking model * serialize errors for tracking * don't assume time is valid * add help docs to UI * surfacing errors to user * language tweak * handle status codes better handling of status codes for mp => sheet; a few UI odds + ends * tests bad input errors * vocabulary * docs refresh good documentation... finally.
1 parent 89a09fa commit c41f7d4

20 files changed

+676
-353
lines changed

Code.js

Lines changed: 114 additions & 69 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,63 @@ each UI has a simple user interface, and is essentially a form you fill out that
3131

3232
## 🗺️ mappings (sheet → mixpanel)
3333

34-
choose the type of data you are importing and then use the visual mapper to connect the events in your **currently active** spreadsheet to the **required fields** for the type of mixpanel data you are importing:
34+
sheet → mixpanel queries your **currently active sheet** to get your sheet's **column headers**.
3535

36-
<img src="https://aktunes.neocities.org/sheets-mixpanel/sheet-to-mp-2.png">
36+
once you choose the type of data you are importing, you will use the visual mapper to connect the **column headers** from your sheet to the **required fields** for the type of mixpanel data you are importing:
37+
38+
<img src="https://aktunes.neocities.org/sheets-mixpanel/mappings.png">
39+
40+
as a brief summary of [the documentation](https://developer.mixpanel.com/docs/data-structure-deep-dive#anatomy-of-an-event) mixpanel's data model for events requires fields for:
41+
- **event name** : what to call each event in mixpanel
42+
- **distinct_id** : the unique user identifier to whom the event is attributed
43+
- **time** : a valid *date* or *time*; if the sheet recognizes your chosen column as a 'date' or time', it should work as intended
44+
- **insert_id** : a value used to deduplicate records (optional)
3745

3846
all other columns in your spreadsheet will get sent as **properties** (event, user, or group)
3947

40-
finally, provide some project information and authentication details.
48+
you'll also need to provide :
49+
- [project id](https://help.mixpanel.com/hc/en-us/articles/115004490503-Project-Settings#project-id)
50+
- [project token](https://help.mixpanel.com/hc/en-us/articles/115004490503-Project-Settings#project-token)
51+
- [project region](https://help.mixpanel.com/hc/en-us/articles/360039135652-Data-Residency-in-EU#enabling-eu-residency)
52+
- either:
53+
- [service account](https://developer.mixpanel.com/reference/service-accounts) (admin or higher)
54+
OR
55+
- [API secret](https://help.mixpanel.com/hc/en-us/articles/115004490503-Project-Settings#api-secret)
56+
57+
note: since v1.12 **syncs** are **not supported** for events.
58+
59+
next, read about [runs + syncs](#sync)
4160

4261

4362
<div id="export"></div>
4463

4564
## 💽 exports (mixpanel → sheet)
65+
mixpanel → sheet queries your mixpanel project for a **report** or **cohort** and makes the results available in a new sheet.
66+
67+
note that this will be identical to the functionality would get when exporting a CSV file from the mixpanel UI:
68+
69+
<img src="https://aktunes.neocities.org/sheets-mixpanel/exportcsv.png">
70+
71+
there are a number of parameters needed to fetch a CSV from mixpanel; the simplest way to gather those parameters is to paste the URL of the report/cohort you wish to sync from your mixpanel project, and the app should find them:
4672

47-
provide authentication details along with a URL of a report or cohort from your mixpanel project that you wish to sync:
48-
<img src="https://aktunes.neocities.org/sheets-mixpanel/mp-to-sheet-2.png">
73+
<img src="https://aktunes.neocities.org/sheets-mixpanel/urlMapper.gif">
4974

50-
the UI will try to resolve all the relevant Ids; in case it cannot, you can add in your information to specify the particular report you want to sync
75+
in case the URL does not contain all the values you need, the UI requires:
76+
- a [service account](https://developer.mixpanel.com/reference/service-accounts) (consumer or higher)
77+
- a URL with either `mixpanel.com` or `eu.mixpanel.com` (to resolve data residency)
78+
- [your project id](https://help.mixpanel.com/hc/en-us/articles/115004490503-Project-Settings#project-id)
79+
- [your workspace id](https://developer.mixpanel.com/reference/query-api-authentication#:~:text=Projects%20with%20Data,a%20request%20parameter.)
5180

52-
only **report** and **cohort** syncs are supported. currently, you can not sync flows reports.
81+
- and either:
82+
- [your report id](https://developer.mixpanel.com/reference/insights-query#:~:text=The%20ID%20of%20your%20Insights%20report%20can%20be%20found%20from%20the%20url%3A%20https%3A//mixpanel.com/report/1/insights%23report/%3CYOUR_BOOKMARK_ID%3E/example%2Dreport)
83+
OR
84+
- [your cohort id](https://developer.mixpanel.com/reference/cohorts-list)
85+
86+
you can manually type these values in after pasting in a URL.
87+
88+
note: as of v1.12 **insights**, **funnels**, & **retention** are the only supported reports
89+
90+
next, read about [runs + syncs](#sync)
5391

5492

5593
<div id="sync"></div>
@@ -63,10 +101,18 @@ each UI has a similar user interface for you to input your details with **four**
63101
- **Run**: run the current configuration **once**; results are display in the UI
64102
- **Sync**: run the current configuration **every hour**; run receipts are stored in a log sheet
65103
- **Save**: store the current configuration
66-
- **Clear**: delete document syncs and delete the current configuration
104+
- **Clear**: delete this sheet's sync and reload the UI
67105

68106
you may only have **one sync** active per sheet at a time.
69107

108+
if you are planning to sync data from your sheet to mixpanel, it is recommended that you do a "run" first.
109+
110+
once created, syncs will run on an hourly schedule; they can _also_ be manually triggered from the main menu by choosing **Sync Now!**:
111+
112+
<img src="https://aktunes.neocities.org/sheets-mixpanel/sync%20now.png">
113+
114+
note: since v1.12 **syncs** are **not supported** for events.
115+
70116

71117

72118
<div id="dev"></div>
@@ -224,6 +270,6 @@ no other sensitive scopes are requested by the application.
224270

225271
## 💬 motivation
226272

227-
google sheets are databases. mixpanel is a database. it should be easy to make these things interoperable.
273+
google sheets are databases. mixpanel is a database. it should be easy to make these things interoperable. now it is!
228274

229275
that's it for now. have fun!

components/dataExport.js

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ DATA OUT OF MP
1111
* @returns {[string, ReportMeta | CohortMeta]} string + metadata `[csv, {}]`
1212
*/
1313
function exportData(config) {
14-
const startTime = Date.now();
15-
const runId = Math.random();
1614
//use last known config if unset
17-
//@ts-ignore
1815
if (!config) config = getConfig();
16+
17+
//@ts-ignore
1918
if (!config.auth) config.auth = validateCreds(config);
2019

2120
const type = config.entity_type;
@@ -55,7 +54,9 @@ function getParams(config) {
5554
const { project_id, workspace_id, region, report_id, auth } = config;
5655
let subdomain = ``;
5756
if (region === "EU") subdomain = `eu.`;
58-
const URL = `https://${subdomain}mixpanel.com/api/app/workspaces/${Number(workspace_id)}/bookmarks/${Number(report_id)}?v=2`;
57+
const URL = `https://${subdomain}mixpanel.com/api/app/workspaces/${Number(workspace_id)}/bookmarks/${Number(
58+
report_id
59+
)}?v=2`;
5960

6061
/** @type {GoogleAppsScript.URL_Fetch.URLFetchRequestOptions} */
6162
const options = {
@@ -67,21 +68,41 @@ function getParams(config) {
6768
muteHttpExceptions: true
6869
};
6970

70-
const res = UrlFetchApp.fetch(URL, options).getContentText();
71-
const data = JSON.parse(res);
71+
const res = UrlFetchApp.fetch(URL, options);
72+
const statusCode = res.getResponseCode();
73+
switch (statusCode) {
74+
case 200:
75+
//noop
76+
break;
77+
case 404:
78+
throw `the report ${report_id || ""} could not be found; check your project, workspace, and report id's and try again`
79+
break;
80+
case 429:
81+
throw `your project has been rate limited; this should resolve by itself`
82+
break;
83+
case 410:
84+
throw ``
85+
case 500:
86+
throw `mixpanel server error; report ${report_id || ""} may no longer exist`;
87+
default:
88+
throw `an unknown error has occurred when getting report ${report_id || ""}'s parameters`;
89+
break;
90+
}
91+
const text = res.getContentText();
92+
93+
const data = JSON.parse(text).results;
7294
const result = {
7395
meta: {
74-
report_type: data?.results?.type || data?.results?.original_type,
75-
report_name: data?.results?.name,
76-
report_desc: data?.results?.description,
77-
report_id: data?.results?.id,
78-
project_id: data?.results?.project_id,
79-
dashboard_id: data?.results?.dashboard_id,
80-
workspace_id: data?.results?.workspace_id,
81-
report_creator:
82-
data?.results?.creator_name || data?.results?.email || data?.results?.creator_id || "unknown"
96+
report_type: data.type || data.original_type,
97+
report_name: data.name,
98+
report_desc: data.description,
99+
report_id: data.id,
100+
project_id: data.project_id,
101+
dashboard_id: data.dashboard_id,
102+
workspace_id: data.workspace_id,
103+
report_creator: data.creator_name || data.email || data.creator_id || "unknown"
83104
},
84-
payload: data?.results?.params
105+
payload: data.params
85106
};
86107

87108
return result;
@@ -100,15 +121,17 @@ function getReportCSV(report_type, params, config) {
100121
if (region === "EU") subdomain = `eu.`;
101122

102123
if (!["insights", "funnels", "retention"].includes(report_type)) {
103-
throw `${report_type} reports are not currently supported for CSV export`;
124+
throw `${report_type || "your supplied"} report is not currently supported for CSV export`;
104125
}
105126

106127
let route = report_type;
107128
if (report_type === "funnels") {
108129
route = `arb_funnels`;
109130
}
110131

111-
const URL = `https://${subdomain}mixpanel.com/api/query/${route}?workspace_id=${Number(workspace_id)}&project_id=${Number(project_id)}`;
132+
const URL = `https://${subdomain}mixpanel.com/api/query/${route}?workspace_id=${Number(
133+
workspace_id
134+
)}&project_id=${Number(project_id)}`;
112135
const payload = {
113136
bookmark: params,
114137
use_query_cache: false,
@@ -121,7 +144,7 @@ function getReportCSV(report_type, params, config) {
121144
headers: {
122145
Authorization: `Basic ${auth}`
123146
},
124-
muteHttpExceptions: true,
147+
muteHttpExceptions: false,
125148
payload: JSON.stringify(payload)
126149
};
127150

@@ -204,7 +227,7 @@ function getCohortMeta(config) {
204227
Authorization: `Basic ${auth}`,
205228
Accept: `application/json`
206229
},
207-
muteHttpExceptions: true
230+
muteHttpExceptions: false
208231
};
209232

210233
const res = JSON.parse(UrlFetchApp.fetch(URL, options).getContentText());

components/dataImport.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,12 @@ DATA INTO MP
1212
* @returns {[ImportResponse[], ImportResults]}
1313
*/
1414
function importData(config, sheet) {
15-
const runId = Math.random();
1615
//use last known config if unset
17-
//@ts-ignore
1816
if (!config) config = getConfig();
17+
18+
//@ts-ignore
1919
if (!config.auth) config.auth = validateCreds(config);
20-
const { record_type } = config;
2120

22-
// console.log('SYNC');
2321
const startTime = Date.now();
2422
let endTime;
2523
const mappings = getMappings(config);
@@ -29,13 +27,12 @@ function importData(config, sheet) {
2927
const sourceData = getJSON(sheet);
3028

3129
// diffing
32-
// todo
3330
const hash = MD5(JSON.stringify(sourceData));
3431
let priorHashes;
3532
try {
36-
priorHashes = JSON.parse(config.hashes); // this isn't working
33+
priorHashes = JSON.parse(config.hashes); // only on sync
3734
} catch (e) {
38-
priorHashes = [];
35+
priorHashes = []; // always on 'run'
3936
}
4037

4138
if (priorHashes.includes(hash)) {
@@ -182,7 +179,7 @@ function getTransformType(config) {
182179
if (config.record_type === "user") return modelMpUsers;
183180
if (config.record_type === "group") return modelMpGroups;
184181
if (config.record_type === "table") return modelMpTables;
185-
throw new Error(`${config.record_type} is not a supported record type`);
182+
throw `${config.record_type} is not a supported record type`;
186183
}
187184

188185
/**
@@ -233,11 +230,12 @@ function summarizeImport(cleanConfig, responses, startTime, endTime, targetData)
233230

234231
if (typeof module !== "undefined") {
235232
module.exports = { importData };
236-
const { getConfig } = require("../utilities/storage.js");
233+
const { getConfig, appendConfig } = require("../utilities/storage.js");
237234
const { validateCreds } = require("../utilities/validate.js");
238235
const { flushToMixpanel } = require("../utilities/flush.js");
239236
const { getJSON } = require("../utilities/toJson.js");
240237
const { clone } = require("../utilities/misc.js");
238+
const { MD5 } = require('../utilities/md5.js')
241239
const { modelMpEvents } = require("../models/modelEvents.js");
242240
const { modelMpUsers } = require("../models/modelUsers.js");
243241
const { modelMpTables } = require("../models/modelTables.js");

0 commit comments

Comments
 (0)