@lightdash/cli 0.1421.0 → 0.1422.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.1422.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.1422.0",
15
+ "@lightdash/warehouses": "^0.1422.0",
16
16
  "@types/columnify": "^1.5.1",
17
17
  "ajv": "^8.11.0",
18
18
  "ajv-formats": "^2.1.1",