@lightdash/cli 0.1421.0 → 0.1423.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -190,11 +190,39 @@ type CliLogin = BaseTrack & {
190
190
  event: 'login.started' | 'login.completed';
191
191
  properties: {
192
192
  userId?: string;
193
+ organizationId?: string;
193
194
  method: string;
194
195
  url: string;
195
196
  };
196
197
  };
197
- type Track = CliGenerateStarted | CliGenerateCompleted | CliGenerateError | CliDbtCommand | CliDbtError | CliPreviewStarted | CliPreviewCompleted | CliPreviewStopped | CliPreviewError | CliRefreshStarted | CliRefreshCompleted | CliRefreshError | CliCompileStarted | CliCompileCompleted | CliCompileError | CliDeployTriggered | CliCreateStarted | CliCreateCompleted | CliCreateError | CliStartStopPreview | CliStopPreviewMissing | CliGenerateExposuresStarted | CliGenerateExposuresCompleted | CliGenerateExposuresError | CliLogin;
198
+ type CliContentAsCode = BaseTrack & ({
199
+ event: 'download.started' | 'upload.started';
200
+ properties: {
201
+ userId?: string;
202
+ organizationId?: string;
203
+ projectId: string;
204
+ };
205
+ } | {
206
+ event: 'download.completed' | 'upload.completed';
207
+ properties: {
208
+ userId?: string;
209
+ organizationId?: string;
210
+ projectId: string;
211
+ chartsNum?: number;
212
+ dashboardsNum?: number;
213
+ timeToCompleted: number;
214
+ };
215
+ } | {
216
+ event: 'download.error' | 'upload.error';
217
+ properties: {
218
+ userId?: string;
219
+ organizationId?: string;
220
+ projectId: string;
221
+ type?: 'charts' | 'dashboards';
222
+ error: string;
223
+ };
224
+ });
225
+ type Track = CliGenerateStarted | CliGenerateCompleted | CliGenerateError | CliDbtCommand | CliDbtError | CliPreviewStarted | CliPreviewCompleted | CliPreviewStopped | CliPreviewError | CliRefreshStarted | CliRefreshCompleted | CliRefreshError | CliCompileStarted | CliCompileCompleted | CliCompileError | CliDeployTriggered | CliCreateStarted | CliCreateCompleted | CliCreateError | CliStartStopPreview | CliStopPreviewMissing | CliGenerateExposuresStarted | CliGenerateExposuresCompleted | CliGenerateExposuresError | CliLogin | CliContentAsCode;
198
226
  export declare class LightdashAnalytics {
199
227
  static track(payload: Track): Promise<void>;
200
228
  }
@@ -27,6 +27,7 @@ const identifyUser = async () => {
27
27
  return {
28
28
  anonymousUuid: config.user?.anonymousUuid,
29
29
  userUuid: config.user?.userUuid,
30
+ organizationUuid: config.user?.organizationUuid,
30
31
  };
31
32
  };
32
33
  class LightdashAnalytics {
package/dist/config.d.ts CHANGED
@@ -3,6 +3,7 @@ export type Config = {
3
3
  user?: {
4
4
  userUuid?: string;
5
5
  anonymousUuid?: string;
6
+ organizationUuid?: string;
6
7
  };
7
8
  context?: {
8
9
  serverUrl?: string;
@@ -22,6 +23,6 @@ export declare const getConfig: () => Promise<Config>;
22
23
  export declare const setProject: (projectUuid: string, projectName: string) => Promise<void>;
23
24
  export declare const setPreviewProject: (projectUuid: string, name: string) => Promise<void>;
24
25
  export declare const unsetPreviewProject: () => Promise<void>;
25
- export declare const setDefaultUser: (userUuid: string) => Promise<void>;
26
+ export declare const setDefaultUser: (userUuid: string, organizationUuid: string) => Promise<void>;
26
27
  export declare const setContext: (context: Config['context']) => Promise<void>;
27
28
  export declare const setAnswer: (answer: Config['answers']) => Promise<void>;
package/dist/config.js CHANGED
@@ -93,13 +93,14 @@ const unsetPreviewProject = async () => {
93
93
  });
94
94
  };
95
95
  exports.unsetPreviewProject = unsetPreviewProject;
96
- const setDefaultUser = async (userUuid) => {
96
+ const setDefaultUser = async (userUuid, organizationUuid) => {
97
97
  const config = await getRawConfig();
98
98
  await (0, exports.setConfig)({
99
99
  ...config,
100
100
  user: {
101
101
  ...(config.user || {}),
102
102
  userUuid,
103
+ organizationUuid,
103
104
  },
104
105
  });
105
106
  };
@@ -8,6 +8,7 @@ const common_1 = require("@lightdash/common");
8
8
  const fs_1 = require("fs");
9
9
  const yaml = tslib_1.__importStar(require("js-yaml"));
10
10
  const path = tslib_1.__importStar(require("path"));
11
+ const analytics_1 = require("../analytics/analytics");
11
12
  const config_1 = require("../config");
12
13
  const globalState_1 = tslib_1.__importDefault(require("../globalState"));
13
14
  const styles = tslib_1.__importStar(require("../styles"));
@@ -95,64 +96,104 @@ const downloadHandler = async (options) => {
95
96
  if (!projectId) {
96
97
  throw new Error('No project selected. Run lightdash config set-project');
97
98
  }
98
- // If any filter is provided, we skip those items without filters
99
- // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
100
- const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
101
- // Download charts
102
- if (hasFilters && options.charts.length === 0) {
103
- console.info(styles.warning(`No charts filters provided, skipping`));
104
- }
105
- else {
106
- const spinner = globalState_1.default.startSpinner(`Downloading charts`);
107
- const chartFilters = parseContentFilters(options.charts);
108
- let chartsAsCode;
109
- let offset = 0;
110
- do {
111
- globalState_1.default.debug(`Downloading charts with offset "${offset}" and filters "${chartFilters}"`);
112
- const queryParams = chartFilters
113
- ? `${chartFilters}&offset=${offset}`
114
- : `?offset=${offset}`;
115
- chartsAsCode = await (0, apiClient_1.lightdashApi)({
116
- method: 'GET',
117
- url: `/api/v1/projects/${projectId}/charts/code${queryParams}`,
118
- body: undefined,
119
- });
120
- spinner.start(`Downloaded ${chartsAsCode.offset} of ${chartsAsCode.total} charts`);
121
- chartsAsCode.missingIds.forEach((missingId) => {
122
- console.warn(styles.warning(`\nNo chart with id "${missingId}"`));
123
- });
124
- await dumpIntoFiles('charts', chartsAsCode.charts);
125
- offset = chartsAsCode.offset;
126
- } while (chartsAsCode.offset < chartsAsCode.total);
127
- spinner.succeed(`Downloaded ${chartsAsCode.total} charts`);
128
- }
129
- // Download dashboards
130
- if (hasFilters && options.dashboards.length === 0) {
131
- console.info(styles.warning(`No dashboards filters provided, skipping`));
99
+ // For analytics
100
+ let chartTotal;
101
+ let dashboardTotal;
102
+ const start = Date.now();
103
+ await analytics_1.LightdashAnalytics.track({
104
+ event: 'download.started',
105
+ properties: {
106
+ userId: config.user?.userUuid,
107
+ organizationId: config.user?.organizationUuid,
108
+ projectId,
109
+ },
110
+ });
111
+ try {
112
+ // If any filter is provided, we skip those items without filters
113
+ // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
114
+ const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
115
+ // Download charts
116
+ if (hasFilters && options.charts.length === 0) {
117
+ console.info(styles.warning(`No charts filters provided, skipping`));
118
+ }
119
+ else {
120
+ const spinner = globalState_1.default.startSpinner(`Downloading charts`);
121
+ const chartFilters = parseContentFilters(options.charts);
122
+ let chartsAsCode;
123
+ let offset = 0;
124
+ do {
125
+ globalState_1.default.debug(`Downloading charts with offset "${offset}" and filters "${chartFilters}"`);
126
+ const queryParams = chartFilters
127
+ ? `${chartFilters}&offset=${offset}`
128
+ : `?offset=${offset}`;
129
+ chartsAsCode = await (0, apiClient_1.lightdashApi)({
130
+ method: 'GET',
131
+ url: `/api/v1/projects/${projectId}/charts/code${queryParams}`,
132
+ body: undefined,
133
+ });
134
+ spinner.start(`Downloaded ${chartsAsCode.offset} of ${chartsAsCode.total} charts`);
135
+ chartsAsCode.missingIds.forEach((missingId) => {
136
+ console.warn(styles.warning(`\nNo chart with id "${missingId}"`));
137
+ });
138
+ await dumpIntoFiles('charts', chartsAsCode.charts);
139
+ offset = chartsAsCode.offset;
140
+ } while (chartsAsCode.offset < chartsAsCode.total);
141
+ chartTotal = chartsAsCode.total;
142
+ spinner.succeed(`Downloaded ${chartsAsCode.total} charts`);
143
+ }
144
+ // Download dashboards
145
+ if (hasFilters && options.dashboards.length === 0) {
146
+ console.info(styles.warning(`No dashboards filters provided, skipping`));
147
+ }
148
+ else {
149
+ const spinner = globalState_1.default.startSpinner(`Downloading dashboards`);
150
+ const dashboardFilters = parseContentFilters(options.dashboards);
151
+ let offset = 0;
152
+ let dashboardsAsCode;
153
+ do {
154
+ globalState_1.default.debug(`Downloading dashboards with offset "${offset}" and filters "${dashboardFilters}"`);
155
+ const queryParams = dashboardFilters
156
+ ? `${dashboardFilters}&offset=${offset}`
157
+ : `?offset=${offset}`;
158
+ dashboardsAsCode = await (0, apiClient_1.lightdashApi)({
159
+ method: 'GET',
160
+ url: `/api/v1/projects/${projectId}/dashboards/code${queryParams}`,
161
+ body: undefined,
162
+ });
163
+ dashboardsAsCode.missingIds.forEach((missingId) => {
164
+ console.warn(styles.warning(`\nNo dashboard with id "${missingId}"`));
165
+ });
166
+ spinner?.start(`Downloaded ${dashboardsAsCode.offset} of ${dashboardsAsCode.total} dashboards`);
167
+ await dumpIntoFiles('dashboards', dashboardsAsCode.dashboards);
168
+ offset = dashboardsAsCode.offset;
169
+ } while (dashboardsAsCode.offset < dashboardsAsCode.total);
170
+ dashboardTotal = dashboardsAsCode.total;
171
+ spinner.succeed(`Downloaded ${dashboardsAsCode.total} dashboards`);
172
+ }
173
+ const end = Date.now();
174
+ await analytics_1.LightdashAnalytics.track({
175
+ event: 'download.completed',
176
+ properties: {
177
+ userId: config.user?.userUuid,
178
+ organizationId: config.user?.organizationUuid,
179
+ projectId,
180
+ chartsNum: chartTotal,
181
+ dashboardsNum: dashboardTotal,
182
+ timeToCompleted: (end - start) / 1000, // in seconds
183
+ },
184
+ });
132
185
  }
133
- else {
134
- const spinner = globalState_1.default.startSpinner(`Downloading dashboards`);
135
- const dashboardFilters = parseContentFilters(options.dashboards);
136
- let offset = 0;
137
- let dashboardsAsCode;
138
- do {
139
- globalState_1.default.debug(`Downloading dashboards with offset "${offset}" and filters "${dashboardFilters}"`);
140
- const queryParams = dashboardFilters
141
- ? `${dashboardFilters}&offset=${offset}`
142
- : `?offset=${offset}`;
143
- dashboardsAsCode = await (0, apiClient_1.lightdashApi)({
144
- method: 'GET',
145
- url: `/api/v1/projects/${projectId}/dashboards/code${queryParams}`,
146
- body: undefined,
147
- });
148
- dashboardsAsCode.missingIds.forEach((missingId) => {
149
- console.warn(styles.warning(`\nNo dashboard with id "${missingId}"`));
150
- });
151
- spinner?.start(`Downloaded ${dashboardsAsCode.offset} of ${dashboardsAsCode.total} dashboards`);
152
- await dumpIntoFiles('dashboards', dashboardsAsCode.dashboards);
153
- offset = dashboardsAsCode.offset;
154
- } while (dashboardsAsCode.offset < dashboardsAsCode.total);
155
- spinner.succeed(`Downloaded ${dashboardsAsCode.total} dashboards`);
186
+ catch (error) {
187
+ console.error(styles.error(`\nError downloading ${error}`));
188
+ await analytics_1.LightdashAnalytics.track({
189
+ event: 'download.error',
190
+ properties: {
191
+ userId: config.user?.userUuid,
192
+ organizationId: config.user?.organizationUuid,
193
+ projectId,
194
+ error: `${error}`,
195
+ },
196
+ });
156
197
  }
157
198
  // TODO delete files if chart don't exist ?*/
158
199
  };
@@ -203,6 +244,7 @@ const logUploadChanges = (changes) => {
203
244
  * @param slugs if slugs are provided, we only force upsert the charts/dashboards that match the slugs, if slugs are empty, we upload files that were locally updated
204
245
  */
205
246
  const upsertResources = async (type, projectId, changes, force, slugs) => {
247
+ const config = await (0, config_1.getConfig)();
206
248
  const items = await readCodeFiles(type);
207
249
  console.info(`Found ${items.length} ${type} files`);
208
250
  const hasFilter = slugs.length > 0;
@@ -241,10 +283,20 @@ const upsertResources = async (type, projectId, changes, force, slugs) => {
241
283
  catch (error) {
242
284
  changes[`${type} with errors`] =
243
285
  (changes[`${type} with errors`] ?? 0) + 1;
244
- console.error(`Error upserting ${type}`, error);
286
+ console.error(styles.error(`Error upserting ${type}: ${error}`));
287
+ await analytics_1.LightdashAnalytics.track({
288
+ event: 'download.error',
289
+ properties: {
290
+ userId: config.user?.userUuid,
291
+ organizationId: config.user?.organizationUuid,
292
+ projectId,
293
+ type,
294
+ error: `${error}`,
295
+ },
296
+ });
245
297
  }
246
298
  }
247
- return changes;
299
+ return { changes, total: filteredItems.length };
248
300
  };
249
301
  const uploadHandler = async (options) => {
250
302
  globalState_1.default.setVerbose(options.verbose);
@@ -258,21 +310,63 @@ const uploadHandler = async (options) => {
258
310
  throw new Error('No project selected. Run lightdash config set-project');
259
311
  }
260
312
  let changes = {};
261
- // If any filter is provided, we skip those items without filters
262
- // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
263
- const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
264
- if (hasFilters && options.charts.length === 0) {
265
- console.info(styles.warning(`No charts filters provided, skipping`));
266
- }
267
- else {
268
- changes = await upsertResources('charts', projectId, changes, options.force, options.charts);
269
- }
270
- if (hasFilters && options.dashboards.length === 0) {
271
- console.info(styles.warning(`No dashboard filters provided, skipping`));
313
+ // For analytics
314
+ let chartTotal;
315
+ let dashboardTotal;
316
+ const start = Date.now();
317
+ await analytics_1.LightdashAnalytics.track({
318
+ event: 'upload.started',
319
+ properties: {
320
+ userId: config.user?.userUuid,
321
+ organizationId: config.user?.organizationUuid,
322
+ projectId,
323
+ },
324
+ });
325
+ try {
326
+ // If any filter is provided, we skip those items without filters
327
+ // eg: if a --charts filter is provided, we skip dashboards if no --dashboards filter is provided
328
+ const hasFilters = options.charts.length > 0 || options.dashboards.length > 0;
329
+ if (hasFilters && options.charts.length === 0) {
330
+ console.info(styles.warning(`No charts filters provided, skipping`));
331
+ }
332
+ else {
333
+ const { changes: chartChanges, total } = await upsertResources('charts', projectId, changes, options.force, options.charts);
334
+ changes = chartChanges;
335
+ chartTotal = total;
336
+ }
337
+ if (hasFilters && options.dashboards.length === 0) {
338
+ console.info(styles.warning(`No dashboard filters provided, skipping`));
339
+ }
340
+ else {
341
+ const { changes: dashboardChanges, total } = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards);
342
+ changes = dashboardChanges;
343
+ dashboardTotal = total;
344
+ }
345
+ const end = Date.now();
346
+ await analytics_1.LightdashAnalytics.track({
347
+ event: 'upload.completed',
348
+ properties: {
349
+ userId: config.user?.userUuid,
350
+ organizationId: config.user?.organizationUuid,
351
+ projectId,
352
+ chartsNum: chartTotal,
353
+ dashboardsNum: dashboardTotal,
354
+ timeToCompleted: (end - start) / 1000, // in seconds
355
+ },
356
+ });
357
+ logUploadChanges(changes);
272
358
  }
273
- else {
274
- changes = await upsertResources('dashboards', projectId, changes, options.force, options.dashboards);
359
+ catch (error) {
360
+ console.error(styles.error(`\nError downloading ${error}`));
361
+ await analytics_1.LightdashAnalytics.track({
362
+ event: 'download.error',
363
+ properties: {
364
+ userId: config.user?.userUuid,
365
+ organizationId: config.user?.organizationUuid,
366
+ projectId,
367
+ error: `${error}`,
368
+ },
369
+ });
275
370
  }
276
- logUploadChanges(changes);
277
371
  };
278
372
  exports.uploadHandler = uploadHandler;
@@ -30,9 +30,10 @@ const loginWithToken = async (url, token, proxyAuthorization) => {
30
30
  throw new common_1.AuthorizationError(`Cannot sign in with token:\n${JSON.stringify(await response.json())}`);
31
31
  }
32
32
  const userBody = await response.json();
33
- const { userUuid } = userBody;
33
+ const { userUuid, organizationUuid } = userBody;
34
34
  return {
35
35
  userUuid,
36
+ organizationUuid,
36
37
  token,
37
38
  };
38
39
  };
@@ -71,7 +72,7 @@ const loginWithPassword = async (url) => {
71
72
  if (header === null) {
72
73
  throw new common_1.AuthorizationError(`Cannot sign in:\n${JSON.stringify(loginBody)}`);
73
74
  }
74
- const { userUuid } = loginBody.results;
75
+ const { userUuid, organizationUuid } = loginBody.results;
75
76
  const cookie = header.split(';')[0].split('=')[1];
76
77
  const patUrl = new url_1.URL(`/api/v1/user/me/personal-access-tokens`, url).href;
77
78
  const now = new Date();
@@ -90,6 +91,7 @@ const loginWithPassword = async (url) => {
90
91
  const { token } = patResponseBody.results;
91
92
  return {
92
93
  userUuid,
94
+ organizationUuid,
93
95
  token,
94
96
  };
95
97
  };
@@ -109,7 +111,7 @@ const login = async (url, options) => {
109
111
  console.error(`\n${styles.title('Warning')}: Login URL ${styles.secondary(url)} does not match a valid cloud server, perhaps you meant ${styles.secondary(cloudServer)} ?\n`);
110
112
  }
111
113
  const proxyAuthorization = process.env.LIGHTDASH_PROXY_AUTHORIZATION;
112
- const { userUuid, token } = options.token
114
+ const { userUuid, token, organizationUuid } = options.token
113
115
  ? await loginWithToken(url, options.token, proxyAuthorization)
114
116
  : await loginWithPassword(url);
115
117
  globalState_1.default.debug(`> Logged in with userUuid: ${userUuid}`);
@@ -117,13 +119,14 @@ const login = async (url, options) => {
117
119
  event: 'login.completed',
118
120
  properties: {
119
121
  userId: userUuid,
122
+ organizationId: organizationUuid,
120
123
  url,
121
124
  method: options.token ? 'token' : 'password',
122
125
  },
123
126
  });
124
127
  await (0, config_1.setContext)({ serverUrl: url, apiKey: token });
125
128
  globalState_1.default.debug(`> Saved config on: ${config_1.configFilePath}`);
126
- await (0, config_1.setDefaultUser)(userUuid);
129
+ await (0, config_1.setDefaultUser)(userUuid, organizationUuid);
127
130
  console.error(`\n ✅️ Login successful\n`);
128
131
  try {
129
132
  if (process.env.CI === 'true') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/cli",
3
- "version": "0.1421.0",
3
+ "version": "0.1423.0",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "lightdash": "dist/index.js"
@@ -11,8 +11,8 @@
11
11
  ],
12
12
  "dependencies": {
13
13
  "@actions/core": "^1.11.1",
14
- "@lightdash/common": "^0.1421.0",
15
- "@lightdash/warehouses": "^0.1421.0",
14
+ "@lightdash/common": "^0.1423.0",
15
+ "@lightdash/warehouses": "^0.1423.0",
16
16
  "@types/columnify": "^1.5.1",
17
17
  "ajv": "^8.11.0",
18
18
  "ajv-formats": "^2.1.1",