@learnpack/learnpack 2.1.52 → 2.1.54

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.
@@ -1,346 +1,353 @@
1
- import { IFile } from "../models/file"
2
- import API from "../utils/api"
3
- import Console from "../utils/console"
4
-
5
- const fs = require("fs")
6
-
7
- function createUUID(): string {
8
- return (
9
- Math.random().toString(36).slice(2, 10) +
10
- Math.random().toString(36).slice(2, 10)
11
- )
12
- }
13
-
14
- function stringToBase64(input: string): string {
15
- return Buffer.from(input).toString("base64")
16
- }
17
-
18
- type TCompilationAttempt = {
19
- source_code: string;
20
- stdout: string;
21
- exit_code: number;
22
- starting_at: number;
23
- ending_at: number;
24
- };
25
-
26
- type TTestAttempt = {
27
- source_code: string;
28
- stdout: string;
29
- exit_code: number;
30
- starting_at: number;
31
- ending_at: number;
32
- };
33
-
34
- type TAIInteraction = {
35
- student_message: string;
36
- source_code: string;
37
- ai_response: string;
38
- starting_at: number;
39
- ending_at: number;
40
- };
41
-
42
- export type TStep = {
43
- slug: string;
44
- position: number;
45
- files: IFile[];
46
- is_testeable: boolean;
47
- opened_at?: number; // The time when the step was opened
48
- completed_at?: number; // If the step has tests, the time when all the tests passed, else, the time when the user opens the next step
49
- compilations: TCompilationAttempt[]; // Everytime the user tries to compile the code
50
- tests: TTestAttempt[]; // Everytime the user tries to run the tests
51
- ai_interactions: TAIInteraction[]; // Everytime the user interacts with the AI
52
- };
53
-
54
- type TWorkoutSession = {
55
- started_at: number;
56
- ended_at?: number;
57
- };
58
-
59
- type TStudent = {
60
- token: string;
61
- user_id: string;
62
- email: string;
63
- };
64
-
65
- export interface ITelemetryJSONSchema {
66
- telemetry_id?: string;
67
- user_id?: number | string;
68
- slug: string;
69
- agent?: string;
70
- tutorial_started_at?: number;
71
- last_interaction_at?: number;
72
- steps: Array<TStep>; // The steps should be the same as the exercise
73
- workout_session: TWorkoutSession[]; // It start when the user starts Learnpack, if the last_interaction_at is available, it automatically fills with that
74
- // number and start another session
75
- }
76
-
77
- type TStepEvent = "compile" | "test" | "ai_interaction" | "open_step";
78
-
79
- export type TTelemetryUrls = {
80
- streaming?: string;
81
- batch?: string;
82
- };
83
-
84
- interface ITelemetryManager {
85
- current: ITelemetryJSONSchema | null;
86
- configPath: string | null;
87
- urls: TTelemetryUrls;
88
- salute: (message: string) => void;
89
- start: (
90
- agent: string,
91
- steps: TStep[],
92
- path: string,
93
- tutorialSlug: string
94
- ) => void;
95
- prevStep?: number;
96
- registerStepEvent: (
97
- stepPosition: number,
98
- event: TStepEvent,
99
- data: any
100
- ) => void;
101
- streamEvent: (stepPosition: number, event: string, data: any) => void;
102
- submit: () => Promise<void>;
103
- finishWorkoutSession: () => void;
104
- setStudent: (student: TStudent) => void;
105
- save: () => void;
106
- retrieve: (
107
- agent: string,
108
- steps: TStep[]
109
- ) => Promise<ITelemetryJSONSchema | null>;
110
- }
111
-
112
- const TelemetryManager: ITelemetryManager = {
113
- current: null,
114
- urls: {},
115
- configPath: "",
116
- salute: message => {
117
- Console.info(message)
118
- },
119
-
120
- start: function (agent, steps, path, tutorialSlug) {
121
- this.configPath = path
122
- if (!this.current) {
123
- this.retrieve(agent, steps)
124
- .then(data => {
125
- const prevTelemetry = data
126
- if (prevTelemetry) {
127
- this.current = prevTelemetry
128
- this.finishWorkoutSession()
129
- } else {
130
- this.current = {
131
- telemetry_id: createUUID(),
132
- slug: tutorialSlug,
133
- agent,
134
- tutorial_started_at: Date.now(),
135
- steps,
136
- workout_session: [
137
- {
138
- started_at: Date.now(),
139
- },
140
- ],
141
- }
142
- }
143
-
144
- this.save()
145
- this.submit()
146
- })
147
- .catch(error => {
148
- Console.debug(error)
149
- })
150
- }
151
- },
152
- // verifyStudent: function () {
153
- // if (!this.current) {
154
- // return;
155
- // }
156
-
157
- // if (!this.current.user_id) {
158
-
159
- // }
160
- // },
161
-
162
- setStudent: function (student) {
163
- if (!this.current) {
164
- return
165
- }
166
-
167
- this.current.user_id = student.user_id
168
- this.save()
169
- this.submit()
170
- },
171
- finishWorkoutSession: function () {
172
- if (!this.current) {
173
- return
174
- }
175
-
176
- const lastSession =
177
- this.current?.workout_session[this.current.workout_session.length - 1]
178
- if (
179
- lastSession &&
180
- !lastSession.ended_at &&
181
- this.current?.last_interaction_at
182
- ) {
183
- lastSession.ended_at = this.current.last_interaction_at
184
- this.current.workout_session.push({
185
- started_at: Date.now(),
186
- })
187
- }
188
- },
189
-
190
- registerStepEvent: function (stepPosition, event, data) {
191
- if (!this.current) {
192
- // throw new Error("Telemetry has not been started");
193
- return
194
- }
195
-
196
- const step = this.current.steps[stepPosition]
197
- if (!step) {
198
- return
199
- }
200
-
201
- if (data.source_code) {
202
- data.source_code = stringToBase64(data.source_code)
203
- }
204
-
205
- if (data.stdout) {
206
- data.stdout = stringToBase64(data.stdout)
207
- }
208
-
209
- if (data.stderr) {
210
- data.stderr = stringToBase64(data.stderr)
211
- }
212
-
213
- if (Object.prototype.hasOwnProperty.call(data, "exitCode")) {
214
- data.exit_code = data.exitCode
215
- data.exitCode = undefined
216
- }
217
-
218
- switch (event) {
219
- case "compile":
220
- if (!step.compilations) {
221
- step.compilations = []
222
- }
223
-
224
- step.compilations.push(data)
225
- this.current.steps[stepPosition] = step
226
- break
227
- case "test":
228
- if (!step.tests) {
229
- step.tests = []
230
- }
231
-
232
- // data.stdout =
233
- step.tests.push(data)
234
- if (data.exit_code === 0) {
235
- step.completed_at = Date.now()
236
- }
237
-
238
- this.current.steps[stepPosition] = step
239
- break
240
- case "ai_interaction":
241
- if (!step.ai_interactions) {
242
- step.ai_interactions = []
243
- }
244
-
245
- step.ai_interactions.push(data)
246
- break
247
- case "open_step": {
248
- const now = Date.now()
249
-
250
- if (!step.opened_at) {
251
- step.opened_at = now
252
- this.current.steps[stepPosition] = step
253
- }
254
-
255
- if (this.prevStep || this.prevStep === 0) {
256
- const prevStep = this.current.steps[this.prevStep]
257
- if (!prevStep.is_testeable && !prevStep.completed_at) {
258
- prevStep.completed_at = now
259
- this.current.steps[this.prevStep] = prevStep
260
- }
261
- }
262
-
263
- this.prevStep = stepPosition
264
-
265
- this.submit()
266
- break
267
- }
268
-
269
- default:
270
- throw new Error(`Event type ${event} is not supported`)
271
- }
272
-
273
- this.current.last_interaction_at = Date.now()
274
- this.streamEvent(stepPosition, event, data)
275
- this.save()
276
- },
277
- retrieve: function () {
278
- return new Promise((resolve, reject) => {
279
- fs.readFile(
280
- `${this.configPath}/telemetry.json`,
281
- "utf8",
282
- (err: any, data: any) => {
283
- if (err) {
284
- if (err.code === "ENOENT") {
285
- // File does not exist, resolve with undefined
286
- resolve(null)
287
- } else {
288
- reject(err)
289
- }
290
- } else {
291
- resolve(JSON.parse(data))
292
- }
293
- }
294
- )
295
- })
296
- },
297
- submit: async function () {
298
- Console.debug("Submitting telemetry...")
299
-
300
- if (!this.current)
301
- return Promise.resolve()
302
- const url = this.urls.batch
303
- if (!url) {
304
- return
305
- }
306
-
307
- const body = this.current
308
-
309
- API.sendBatchTelemetry(url, body)
310
- },
311
- save: function () {
312
- fs.writeFile(
313
- `${this.configPath}/telemetry.json`,
314
- JSON.stringify(this.current),
315
- (err: any) => {
316
- if (err)
317
- throw err
318
- }
319
- )
320
- },
321
-
322
- streamEvent: async function (stepPosition, event, data) {
323
- if (!this.current)
324
- return
325
-
326
- const url = this.urls.streaming
327
- if (!url) {
328
- return
329
- }
330
-
331
- const stepSlug = this.current.steps[stepPosition].slug
332
-
333
- const body = {
334
- slug: stepSlug,
335
- telemetry_id: this.current.telemetry_id,
336
- user_id: this.current.user_id,
337
- step_position: stepPosition,
338
- event,
339
- data,
340
- }
341
-
342
- API.sendStreamTelemetry(url, body)
343
- },
344
- }
345
-
346
- export default TelemetryManager
1
+ import { IFile } from "../models/file"
2
+ import API from "../utils/api"
3
+ import Console from "../utils/console"
4
+
5
+ const fs = require("fs")
6
+
7
+ function createUUID(): string {
8
+ return (
9
+ Math.random().toString(36).slice(2, 10) +
10
+ Math.random().toString(36).slice(2, 10)
11
+ )
12
+ }
13
+
14
+ function stringToBase64(input: string): string {
15
+ return Buffer.from(input).toString("base64")
16
+ }
17
+
18
+ type TCompilationAttempt = {
19
+ source_code: string;
20
+ stdout: string;
21
+ exit_code: number;
22
+ starting_at: number;
23
+ ending_at: number;
24
+ };
25
+
26
+ type TTestAttempt = {
27
+ source_code: string;
28
+ stdout: string;
29
+ exit_code: number;
30
+ starting_at: number;
31
+ ending_at: number;
32
+ };
33
+
34
+ type TAIInteraction = {
35
+ student_message: string;
36
+ source_code: string;
37
+ ai_response: string;
38
+ starting_at: number;
39
+ ending_at: number;
40
+ };
41
+
42
+ export type TStep = {
43
+ slug: string;
44
+ position: number;
45
+ files: IFile[];
46
+ is_testeable: boolean;
47
+ opened_at?: number; // The time when the step was opened
48
+ completed_at?: number; // If the step has tests, the time when all the tests passed, else, the time when the user opens the next step
49
+ compilations: TCompilationAttempt[]; // Everytime the user tries to compile the code
50
+ tests: TTestAttempt[]; // Everytime the user tries to run the tests
51
+ ai_interactions: TAIInteraction[]; // Everytime the user interacts with the AI
52
+ };
53
+
54
+ type TWorkoutSession = {
55
+ started_at: number;
56
+ ended_at?: number;
57
+ };
58
+
59
+ type TStudent = {
60
+ token: string;
61
+ user_id: string;
62
+ email: string;
63
+ };
64
+
65
+ export interface ITelemetryJSONSchema {
66
+ telemetry_id?: string;
67
+ user_id?: number | string;
68
+ slug: string;
69
+ agent?: string;
70
+ tutorial_started_at?: number;
71
+ last_interaction_at?: number;
72
+ steps: Array<TStep>; // The steps should be the same as the exercise
73
+ workout_session: TWorkoutSession[]; // It start when the user starts Learnpack, if the last_interaction_at is available, it automatically fills with that
74
+ // number and start another session
75
+ }
76
+
77
+ type TStepEvent = "compile" | "test" | "ai_interaction" | "open_step";
78
+
79
+ export type TTelemetryUrls = {
80
+ streaming?: string;
81
+ batch?: string;
82
+ };
83
+
84
+ interface ITelemetryManager {
85
+ current: ITelemetryJSONSchema | null;
86
+ configPath: string | null;
87
+ urls: TTelemetryUrls;
88
+ salute: (message: string) => void;
89
+ start: (
90
+ agent: string,
91
+ steps: TStep[],
92
+ path: string,
93
+ tutorialSlug: string
94
+ ) => void;
95
+ prevStep?: number;
96
+ registerStepEvent: (
97
+ stepPosition: number,
98
+ event: TStepEvent,
99
+ data: any
100
+ ) => void;
101
+ streamEvent: (stepPosition: number, event: string, data: any) => void;
102
+ submit: () => Promise<void>;
103
+ finishWorkoutSession: () => void;
104
+ setStudent: (student: TStudent) => void;
105
+ save: () => void;
106
+ retrieve: () => Promise<ITelemetryJSONSchema | null>;
107
+ }
108
+
109
+ const TelemetryManager: ITelemetryManager = {
110
+ current: null,
111
+ urls: {},
112
+ configPath: "",
113
+ salute: message => {
114
+ Console.info(message)
115
+ },
116
+
117
+ start: function (agent, steps, path, tutorialSlug) {
118
+ this.configPath = path
119
+ if (!this.current) {
120
+ this.retrieve()
121
+ .then(data => {
122
+ const prevTelemetry = data
123
+ if (prevTelemetry) {
124
+ this.current = prevTelemetry
125
+ this.finishWorkoutSession()
126
+ } else {
127
+ this.current = {
128
+ telemetry_id: createUUID(),
129
+ slug: tutorialSlug,
130
+ agent,
131
+ tutorial_started_at: Date.now(),
132
+ steps,
133
+ workout_session: [
134
+ {
135
+ started_at: Date.now(),
136
+ },
137
+ ],
138
+ }
139
+ }
140
+
141
+ this.save()
142
+ this.submit()
143
+ })
144
+ .catch(error => {
145
+ Console.debug(error)
146
+ // Delete the telemetry.json if it exists
147
+ fs.unlinkSync(`${this.configPath}/telemetry.json`)
148
+ throw new Error(
149
+ "There was a problem starting, reload LearnPack\nRun\n$ learnpack start"
150
+ )
151
+ })
152
+ }
153
+ },
154
+ // verifyStudent: function () {
155
+ // if (!this.current) {
156
+ // return;
157
+ // }
158
+
159
+ // if (!this.current.user_id) {
160
+
161
+ // }
162
+ // },
163
+
164
+ setStudent: function (student) {
165
+ if (!this.current) {
166
+ return
167
+ }
168
+
169
+ this.current.user_id = student.user_id
170
+ this.save()
171
+ this.submit()
172
+ },
173
+ finishWorkoutSession: function () {
174
+ if (!this.current) {
175
+ return
176
+ }
177
+
178
+ const lastSession =
179
+ this.current?.workout_session[this.current.workout_session.length - 1]
180
+ if (
181
+ lastSession &&
182
+ !lastSession.ended_at &&
183
+ this.current?.last_interaction_at
184
+ ) {
185
+ lastSession.ended_at = this.current.last_interaction_at
186
+ this.current.workout_session.push({
187
+ started_at: Date.now(),
188
+ })
189
+ }
190
+ },
191
+
192
+ registerStepEvent: function (stepPosition, event, data) {
193
+ if (!this.current) {
194
+ // throw new Error("Telemetry has not been started");
195
+ return
196
+ }
197
+
198
+ const step = this.current.steps[stepPosition]
199
+ if (!step) {
200
+ return
201
+ }
202
+
203
+ if (data.source_code) {
204
+ data.source_code = stringToBase64(data.source_code)
205
+ }
206
+
207
+ if (data.stdout) {
208
+ data.stdout = stringToBase64(data.stdout)
209
+ }
210
+
211
+ if (data.stderr) {
212
+ data.stderr = stringToBase64(data.stderr)
213
+ }
214
+
215
+ if (Object.prototype.hasOwnProperty.call(data, "exitCode")) {
216
+ data.exit_code = data.exitCode
217
+ data.exitCode = undefined
218
+ }
219
+
220
+ switch (event) {
221
+ case "compile":
222
+ if (!step.compilations) {
223
+ step.compilations = []
224
+ }
225
+
226
+ step.compilations.push(data)
227
+ this.current.steps[stepPosition] = step
228
+ break
229
+ case "test":
230
+ if (!step.tests) {
231
+ step.tests = []
232
+ }
233
+
234
+ // data.stdout =
235
+ step.tests.push(data)
236
+ if (data.exit_code === 0) {
237
+ step.completed_at = Date.now()
238
+ }
239
+
240
+ this.current.steps[stepPosition] = step
241
+ break
242
+ case "ai_interaction":
243
+ if (!step.ai_interactions) {
244
+ step.ai_interactions = []
245
+ }
246
+
247
+ step.ai_interactions.push(data)
248
+ break
249
+ case "open_step": {
250
+ const now = Date.now()
251
+
252
+ if (!step.opened_at) {
253
+ step.opened_at = now
254
+ this.current.steps[stepPosition] = step
255
+ }
256
+
257
+ if (this.prevStep || this.prevStep === 0) {
258
+ const prevStep = this.current.steps[this.prevStep]
259
+ if (!prevStep.is_testeable && !prevStep.completed_at) {
260
+ prevStep.completed_at = now
261
+ this.current.steps[this.prevStep] = prevStep
262
+ }
263
+ }
264
+
265
+ this.prevStep = stepPosition
266
+
267
+ this.submit()
268
+ break
269
+ }
270
+
271
+ default:
272
+ throw new Error(`Event type ${event} is not supported`)
273
+ }
274
+
275
+ this.current.last_interaction_at = Date.now()
276
+ this.streamEvent(stepPosition, event, data)
277
+ this.save()
278
+ },
279
+ retrieve: function () {
280
+ return new Promise((resolve, reject) => {
281
+ fs.readFile(
282
+ `${this.configPath}/telemetry.json`,
283
+ "utf8",
284
+ (err: any, data: any) => {
285
+ if (err) {
286
+ if (err.code === "ENOENT") {
287
+ // File does not exist, resolve with undefined
288
+ resolve(null)
289
+ } else {
290
+ reject(err)
291
+ }
292
+ } else {
293
+ try {
294
+ resolve(JSON.parse(data))
295
+ } catch (error) {
296
+ reject(error)
297
+ }
298
+ }
299
+ }
300
+ )
301
+ })
302
+ },
303
+
304
+ submit: async function () {
305
+ Console.debug("Submitting telemetry...")
306
+
307
+ if (!this.current)
308
+ return Promise.resolve()
309
+ const url = this.urls.batch
310
+ if (!url) {
311
+ return
312
+ }
313
+
314
+ const body = this.current
315
+
316
+ API.sendBatchTelemetry(url, body)
317
+ },
318
+ save: function () {
319
+ fs.writeFile(
320
+ `${this.configPath}/telemetry.json`,
321
+ JSON.stringify(this.current),
322
+ (err: any) => {
323
+ if (err)
324
+ throw err
325
+ }
326
+ )
327
+ },
328
+
329
+ streamEvent: async function (stepPosition, event, data) {
330
+ if (!this.current)
331
+ return
332
+
333
+ const url = this.urls.streaming
334
+ if (!url) {
335
+ return
336
+ }
337
+
338
+ const stepSlug = this.current.steps[stepPosition].slug
339
+
340
+ const body = {
341
+ slug: stepSlug,
342
+ telemetry_id: this.current.telemetry_id,
343
+ user_id: this.current.user_id,
344
+ step_position: stepPosition,
345
+ event,
346
+ data,
347
+ }
348
+
349
+ API.sendStreamTelemetry(url, body)
350
+ },
351
+ }
352
+
353
+ export default TelemetryManager