@learnpack/learnpack 5.0.3 → 5.0.5
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +10 -10
- package/lib/commands/publish.js +31 -5
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
- package/src/commands/publish.ts +38 -5
- package/src/managers/telemetry.ts +353 -353
package/README.md
CHANGED
@@ -21,7 +21,7 @@ $ npm install -g @learnpack/learnpack
|
|
21
21
|
$ learnpack COMMAND
|
22
22
|
running command...
|
23
23
|
$ learnpack (-v|--version|version)
|
24
|
-
@learnpack/learnpack/5.0.
|
24
|
+
@learnpack/learnpack/5.0.5 win32-x64 node-v20.16.0
|
25
25
|
$ learnpack --help [COMMAND]
|
26
26
|
USAGE
|
27
27
|
$ learnpack COMMAND
|
@@ -74,7 +74,7 @@ DESCRIPTION
|
|
74
74
|
12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)
|
75
75
|
```
|
76
76
|
|
77
|
-
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
77
|
+
_See code: [src\commands\audit.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\audit.ts)_
|
78
78
|
|
79
79
|
## `learnpack clean`
|
80
80
|
|
@@ -89,7 +89,7 @@ DESCRIPTION
|
|
89
89
|
Extra documentation goes here
|
90
90
|
```
|
91
91
|
|
92
|
-
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
92
|
+
_See code: [src\commands\clean.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\clean.ts)_
|
93
93
|
|
94
94
|
## `learnpack download [PACKAGE]`
|
95
95
|
|
@@ -107,7 +107,7 @@ DESCRIPTION
|
|
107
107
|
Extra documentation goes here
|
108
108
|
```
|
109
109
|
|
110
|
-
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
110
|
+
_See code: [src\commands\download.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\download.ts)_
|
111
111
|
|
112
112
|
## `learnpack help [COMMAND]`
|
113
113
|
|
@@ -138,7 +138,7 @@ OPTIONS
|
|
138
138
|
-h, --grading show CLI help
|
139
139
|
```
|
140
140
|
|
141
|
-
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
141
|
+
_See code: [src\commands\init.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\init.ts)_
|
142
142
|
|
143
143
|
## `learnpack login [PACKAGE]`
|
144
144
|
|
@@ -156,7 +156,7 @@ DESCRIPTION
|
|
156
156
|
Extra documentation goes here
|
157
157
|
```
|
158
158
|
|
159
|
-
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
159
|
+
_See code: [src\commands\login.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\login.ts)_
|
160
160
|
|
161
161
|
## `learnpack logout [PACKAGE]`
|
162
162
|
|
@@ -174,7 +174,7 @@ DESCRIPTION
|
|
174
174
|
Extra documentation goes here
|
175
175
|
```
|
176
176
|
|
177
|
-
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
177
|
+
_See code: [src\commands\logout.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\logout.ts)_
|
178
178
|
|
179
179
|
## `learnpack plugins`
|
180
180
|
|
@@ -305,7 +305,7 @@ OPTIONS
|
|
305
305
|
-h, --help show CLI help
|
306
306
|
```
|
307
307
|
|
308
|
-
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
308
|
+
_See code: [src\commands\publish.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\publish.ts)_
|
309
309
|
|
310
310
|
## `learnpack start`
|
311
311
|
|
@@ -326,7 +326,7 @@ OPTIONS
|
|
326
326
|
-w, --watch Watch for file changes
|
327
327
|
```
|
328
328
|
|
329
|
-
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
329
|
+
_See code: [src\commands\start.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\start.ts)_
|
330
330
|
|
331
331
|
## `learnpack test [EXERCISESLUG]`
|
332
332
|
|
@@ -340,7 +340,7 @@ ARGUMENTS
|
|
340
340
|
EXERCISESLUG The name of the exercise to test
|
341
341
|
```
|
342
342
|
|
343
|
-
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.
|
343
|
+
_See code: [src\commands\test.ts](https://github.com/learnpack/learnpack-cli/blob/v5.0.5/src\commands\test.ts)_
|
344
344
|
<!-- commandsstop -->
|
345
345
|
|
346
346
|
> > > > > > > 0cb3e56d84c197f9d008836bb573eade212b7e57
|
package/lib/commands/publish.js
CHANGED
@@ -13,6 +13,8 @@ const FormData = require("form-data");
|
|
13
13
|
const console_1 = require("../utils/console");
|
14
14
|
const file_1 = require("../managers/file");
|
15
15
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com";
|
16
|
+
// const RIGOBOT_HOST =
|
17
|
+
// "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us116.gitpod.io"
|
16
18
|
const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload";
|
17
19
|
class BuildCommand extends SessionCommand_1.default {
|
18
20
|
async init() {
|
@@ -72,13 +74,13 @@ class BuildCommand extends SessionCommand_1.default {
|
|
72
74
|
else {
|
73
75
|
this.error("config.json not found");
|
74
76
|
}
|
75
|
-
// Copy .learn/assets directory
|
77
|
+
// Copy .learn/assets directory, if it exists else create it
|
76
78
|
const assetsDir = path.join(process.cwd(), ".learn", "assets");
|
77
79
|
if (fs.existsSync(assetsDir)) {
|
78
80
|
this.copyDirectory(assetsDir, path.join(buildDir, ".learn", "assets"));
|
79
81
|
}
|
80
82
|
else {
|
81
|
-
|
83
|
+
fs.mkdirSync(path.join(buildDir, ".learn", "assets"), { recursive: true });
|
82
84
|
}
|
83
85
|
// Copy .learn/_app directory files to the same level as config.json
|
84
86
|
const appDir = path.join(process.cwd(), ".learn", "_app");
|
@@ -88,6 +90,27 @@ class BuildCommand extends SessionCommand_1.default {
|
|
88
90
|
else {
|
89
91
|
this.error(".learn/_app directory not found");
|
90
92
|
}
|
93
|
+
// After copying the _app directory
|
94
|
+
const indexHtmlPath = path.join(appDir, "index.html");
|
95
|
+
const buildIndexHtmlPath = path.join(buildDir, "index.html");
|
96
|
+
if (fs.existsSync(indexHtmlPath)) {
|
97
|
+
let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf-8");
|
98
|
+
const description = learnJson.description.us || "LearnPack is awesome!";
|
99
|
+
const title = learnJson.title.us || "LearnPack: Interactive Learning as a Service";
|
100
|
+
const previewUrl = learnJson.preview ||
|
101
|
+
"https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/public/learnpack.svg";
|
102
|
+
// Replace placeholders and the <title>Old title </title> tag for a new tag with the title
|
103
|
+
indexHtmlContent = indexHtmlContent
|
104
|
+
.replace(/{{description}}/g, description)
|
105
|
+
.replace(/<title>.*<\/title>/, `<title>${title}</title>`)
|
106
|
+
.replace(/{{title}}/g, title)
|
107
|
+
.replace(/{{preview}}/g, previewUrl);
|
108
|
+
// Write the modified content to the build directory
|
109
|
+
fs.writeFileSync(buildIndexHtmlPath, indexHtmlContent);
|
110
|
+
}
|
111
|
+
else {
|
112
|
+
this.error("index.html not found in _app directory");
|
113
|
+
}
|
91
114
|
// Copy exercises directory
|
92
115
|
const exercisesDir = path.join(process.cwd(), "exercises");
|
93
116
|
const learnExercisesDir = path.join(process.cwd(), ".learn", "exercises");
|
@@ -110,8 +133,8 @@ class BuildCommand extends SessionCommand_1.default {
|
|
110
133
|
output.on("close", async () => {
|
111
134
|
this.log(`Build completed: ${zipFilePath} (${archive.pointer()} total bytes)`);
|
112
135
|
// Remove build directory after zip is created
|
113
|
-
this.removeDirectory(buildDir)
|
114
|
-
|
136
|
+
// this.removeDirectory(buildDir)
|
137
|
+
console_1.default.debug("Zip file saved in project root");
|
115
138
|
const formData = new FormData();
|
116
139
|
formData.append("file", fs.createReadStream(zipFilePath));
|
117
140
|
formData.append("config", JSON.stringify(learnJson));
|
@@ -120,6 +143,8 @@ class BuildCommand extends SessionCommand_1.default {
|
|
120
143
|
headers: Object.assign(Object.assign({}, formData.getHeaders()), { Authorization: `Token ${rigoToken}` }),
|
121
144
|
});
|
122
145
|
console.log(res.data);
|
146
|
+
// Remove the zip file after uploading
|
147
|
+
fs.unlinkSync(zipFilePath);
|
123
148
|
}
|
124
149
|
catch (error) {
|
125
150
|
if (axios_1.default.isAxiosError(error)) {
|
@@ -130,12 +155,13 @@ class BuildCommand extends SessionCommand_1.default {
|
|
130
155
|
console.error(error.response.data.error);
|
131
156
|
}
|
132
157
|
else {
|
133
|
-
console.error("Error uploading file:", error
|
158
|
+
console.error("Error uploading file:", error);
|
134
159
|
}
|
135
160
|
}
|
136
161
|
else {
|
137
162
|
console.error("Error uploading file:", error);
|
138
163
|
}
|
164
|
+
fs.unlinkSync(zipFilePath);
|
139
165
|
}
|
140
166
|
});
|
141
167
|
archive.on("error", (err) => {
|
package/oclif.manifest.json
CHANGED
@@ -1 +1 @@
|
|
1
|
-
{"version":"5.0.
|
1
|
+
{"version":"5.0.5","commands":{"audit":{"id":"audit","description":"learnpack audit is the command in charge of creating an auditory of the repository\n...\nlearnpack audit checks for the following information in a repository:\n 1. The configuration object has slug, repository and description. (Error)\n 2. The command learnpack clean has been run. (Error)\n 3. If a markdown or test file doesn't have any content. (Error)\n 4. The links are accessing to valid servers. (Error)\n 5. The relative images are working (If they have the shortest path to the image or if the images exists in the assets). (Error)\n 6. The external images are working (If they are pointing to a valid server). (Error)\n 7. The exercises directory names are valid. (Error)\n 8. If an exercise doesn't have a README file. (Error)\n 9. The exercises array (Of the config file) has content. (Error)\n 10. The exercses have the same translations. (Warning)\n 11. The .gitignore file exists. (Warning)\n 12. If there is a file within the exercises folder but not inside of any particular exercise's folder. (Warning)\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"clean":{"id":"clean","description":"Clean the configuration object\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[]},"download":{"id":"download","description":"Describe the command here\n...\nExtra documentation goes here\n","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"init":{"id":"init","description":"Create a new learning package: Book, Tutorial or Exercise","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"grading":{"name":"grading","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"login":{"id":"login","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"logout":{"id":"logout","description":"Describe the command here\n ...\n Extra documentation goes here\n ","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"package","description":"The unique string that identifies this package on learnpack","required":false,"hidden":false}]},"publish":{"id":"publish","description":"Builds the project by copying necessary files and directories into a zip file","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"help":{"name":"help","type":"boolean","char":"h","description":"show CLI help","allowNo":false}},"args":[]},"start":{"id":"start","description":"Runs a small server with all the exercise instructions","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{"port":{"name":"port","type":"option","char":"p","description":"server port"},"host":{"name":"host","type":"option","char":"h","description":"server host"},"disableGrading":{"name":"disableGrading","type":"boolean","char":"D","description":"disble grading functionality","allowNo":false},"watch":{"name":"watch","type":"boolean","char":"w","description":"Watch for file changes","allowNo":false},"editor":{"name":"editor","type":"option","char":"e","description":"[preview, extension]","options":["extension","preview"]},"version":{"name":"version","type":"option","char":"v","description":"E.g: 1.0.1"},"grading":{"name":"grading","type":"option","char":"g","description":"[isolated, incremental]","options":["isolated","incremental"]},"debug":{"name":"debug","type":"boolean","char":"d","description":"debugger mode for more verbage","allowNo":false}},"args":[]},"test":{"id":"test","description":"Test exercises","pluginName":"@learnpack/learnpack","pluginType":"core","aliases":[],"flags":{},"args":[{"name":"exerciseSlug","description":"The name of the exercise to test","required":false,"hidden":false}]}}}
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "@learnpack/learnpack",
|
3
3
|
"description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
|
4
|
-
"version": "5.0.
|
4
|
+
"version": "5.0.5",
|
5
5
|
"author": "Alejandro Sanchez @alesanchezr",
|
6
6
|
"contributors": [
|
7
7
|
{
|
package/src/commands/publish.ts
CHANGED
@@ -16,6 +16,8 @@ import {
|
|
16
16
|
} from "../managers/file"
|
17
17
|
|
18
18
|
const RIGOBOT_HOST = "https://rigobot.herokuapp.com"
|
19
|
+
// const RIGOBOT_HOST =
|
20
|
+
// "https://8000-charlytoc-rigobot-bmwdeam7cev.ws-us116.gitpod.io"
|
19
21
|
const uploadZipEndpont = RIGOBOT_HOST + "/v1/learnpack/upload"
|
20
22
|
|
21
23
|
export default class BuildCommand extends SessionCommand {
|
@@ -100,12 +102,12 @@ export default class BuildCommand extends SessionCommand {
|
|
100
102
|
this.error("config.json not found")
|
101
103
|
}
|
102
104
|
|
103
|
-
// Copy .learn/assets directory
|
105
|
+
// Copy .learn/assets directory, if it exists else create it
|
104
106
|
const assetsDir = path.join(process.cwd(), ".learn", "assets")
|
105
107
|
if (fs.existsSync(assetsDir)) {
|
106
108
|
this.copyDirectory(assetsDir, path.join(buildDir, ".learn", "assets"))
|
107
109
|
} else {
|
108
|
-
|
110
|
+
fs.mkdirSync(path.join(buildDir, ".learn", "assets"), { recursive: true })
|
109
111
|
}
|
110
112
|
|
111
113
|
// Copy .learn/_app directory files to the same level as config.json
|
@@ -116,6 +118,33 @@ export default class BuildCommand extends SessionCommand {
|
|
116
118
|
this.error(".learn/_app directory not found")
|
117
119
|
}
|
118
120
|
|
121
|
+
// After copying the _app directory
|
122
|
+
const indexHtmlPath = path.join(appDir, "index.html")
|
123
|
+
const buildIndexHtmlPath = path.join(buildDir, "index.html")
|
124
|
+
|
125
|
+
if (fs.existsSync(indexHtmlPath)) {
|
126
|
+
let indexHtmlContent = fs.readFileSync(indexHtmlPath, "utf-8")
|
127
|
+
|
128
|
+
const description = learnJson.description.us || "LearnPack is awesome!"
|
129
|
+
const title =
|
130
|
+
learnJson.title.us || "LearnPack: Interactive Learning as a Service"
|
131
|
+
|
132
|
+
const previewUrl =
|
133
|
+
learnJson.preview ||
|
134
|
+
"https://raw.githubusercontent.com/learnpack/ide/refs/heads/master/public/learnpack.svg"
|
135
|
+
// Replace placeholders and the <title>Old title </title> tag for a new tag with the title
|
136
|
+
indexHtmlContent = indexHtmlContent
|
137
|
+
.replace(/{{description}}/g, description)
|
138
|
+
.replace(/<title>.*<\/title>/, `<title>${title}</title>`)
|
139
|
+
.replace(/{{title}}/g, title)
|
140
|
+
.replace(/{{preview}}/g, previewUrl)
|
141
|
+
|
142
|
+
// Write the modified content to the build directory
|
143
|
+
fs.writeFileSync(buildIndexHtmlPath, indexHtmlContent)
|
144
|
+
} else {
|
145
|
+
this.error("index.html not found in _app directory")
|
146
|
+
}
|
147
|
+
|
119
148
|
// Copy exercises directory
|
120
149
|
const exercisesDir = path.join(process.cwd(), "exercises")
|
121
150
|
const learnExercisesDir = path.join(process.cwd(), ".learn", "exercises")
|
@@ -142,8 +171,8 @@ export default class BuildCommand extends SessionCommand {
|
|
142
171
|
`Build completed: ${zipFilePath} (${archive.pointer()} total bytes)`
|
143
172
|
)
|
144
173
|
// Remove build directory after zip is created
|
145
|
-
this.removeDirectory(buildDir)
|
146
|
-
|
174
|
+
// this.removeDirectory(buildDir)
|
175
|
+
Console.debug("Zip file saved in project root")
|
147
176
|
|
148
177
|
const formData = new FormData()
|
149
178
|
formData.append("file", fs.createReadStream(zipFilePath))
|
@@ -157,6 +186,8 @@ export default class BuildCommand extends SessionCommand {
|
|
157
186
|
},
|
158
187
|
})
|
159
188
|
console.log(res.data)
|
189
|
+
// Remove the zip file after uploading
|
190
|
+
fs.unlinkSync(zipFilePath)
|
160
191
|
} catch (error) {
|
161
192
|
if (axios.isAxiosError(error)) {
|
162
193
|
if (error.response && error.response.status === 403) {
|
@@ -164,11 +195,13 @@ export default class BuildCommand extends SessionCommand {
|
|
164
195
|
} else if (error.response && error.response.status === 400) {
|
165
196
|
console.error(error.response.data.error)
|
166
197
|
} else {
|
167
|
-
console.error("Error uploading file:", error
|
198
|
+
console.error("Error uploading file:", error)
|
168
199
|
}
|
169
200
|
} else {
|
170
201
|
console.error("Error uploading file:", error)
|
171
202
|
}
|
203
|
+
|
204
|
+
fs.unlinkSync(zipFilePath)
|
172
205
|
}
|
173
206
|
})
|
174
207
|
|
@@ -1,353 +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: () => 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
|
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
|