@playcademy/sandbox 0.4.1-beta.1 → 0.4.1-beta.2
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.
- package/dist/cli.js +271 -24
- package/dist/constants.js +12 -1
- package/dist/server.js +271 -24
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -243,7 +243,7 @@ var init_platform = __esm(() => {
|
|
|
243
243
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
244
244
|
|
|
245
245
|
// ../constants/src/timeback.ts
|
|
246
|
-
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
246
|
+
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
|
|
247
247
|
var init_timeback2 = __esm(() => {
|
|
248
248
|
TIMEBACK_ROUTES = {
|
|
249
249
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
@@ -288,6 +288,17 @@ var init_timeback2 = __esm(() => {
|
|
|
288
288
|
sortOrder: 1,
|
|
289
289
|
lessonType: "quiz"
|
|
290
290
|
};
|
|
291
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
|
|
292
|
+
xp: 1,
|
|
293
|
+
mastery: 0,
|
|
294
|
+
score: 2
|
|
295
|
+
};
|
|
296
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
|
|
297
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
|
|
298
|
+
mastery: 0,
|
|
299
|
+
time: 60,
|
|
300
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
|
|
301
|
+
};
|
|
291
302
|
});
|
|
292
303
|
|
|
293
304
|
// ../constants/src/cloudflare.ts
|
|
@@ -1065,7 +1076,7 @@ var package_default;
|
|
|
1065
1076
|
var init_package = __esm(() => {
|
|
1066
1077
|
package_default = {
|
|
1067
1078
|
name: "@playcademy/sandbox",
|
|
1068
|
-
version: "0.4.1-beta.
|
|
1079
|
+
version: "0.4.1-beta.2",
|
|
1069
1080
|
description: "Local development server for Playcademy game development",
|
|
1070
1081
|
type: "module",
|
|
1071
1082
|
exports: {
|
|
@@ -29891,7 +29902,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29891
29902
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29892
29903
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29893
29904
|
}
|
|
29894
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema,
|
|
29905
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29895
29906
|
var init_schemas4 = __esm(() => {
|
|
29896
29907
|
init_drizzle_zod();
|
|
29897
29908
|
init_esm();
|
|
@@ -29966,22 +29977,22 @@ var init_schemas4 = __esm(() => {
|
|
|
29966
29977
|
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
29967
29978
|
path: ["masteredUnitsAbsolute"]
|
|
29968
29979
|
});
|
|
29969
|
-
|
|
29980
|
+
GameRunMetricsSchema = exports_external.object({
|
|
29981
|
+
runId: exports_external.string().uuid(),
|
|
29970
29982
|
activityId: exports_external.string().min(1),
|
|
29971
29983
|
activityName: exports_external.string().optional(),
|
|
29972
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29973
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29974
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29975
|
-
|
|
29976
|
-
lastCompletedAt: exports_external.string().datetime().optional()
|
|
29984
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29985
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29986
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29987
|
+
score: exports_external.number().min(0).max(100).optional()
|
|
29977
29988
|
});
|
|
29978
29989
|
GameCourseMetricsSchema = exports_external.object({
|
|
29979
29990
|
grade: TimebackGradeSchema,
|
|
29980
29991
|
subject: TimebackSubjectSchema,
|
|
29981
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29982
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29983
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29984
|
-
activities: exports_external.array(
|
|
29992
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29993
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29994
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29995
|
+
activities: exports_external.array(GameRunMetricsSchema).optional()
|
|
29985
29996
|
});
|
|
29986
29997
|
GameMetricsResponseSchema = exports_external.object({
|
|
29987
29998
|
studentId: exports_external.string().min(1),
|
|
@@ -30258,6 +30269,124 @@ var init_timeback_admin_util = __esm(() => {
|
|
|
30258
30269
|
init_errors();
|
|
30259
30270
|
});
|
|
30260
30271
|
|
|
30272
|
+
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30273
|
+
function createMetricRow(definition) {
|
|
30274
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30275
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
30276
|
+
return null;
|
|
30277
|
+
}
|
|
30278
|
+
if (gameValue === undefined) {
|
|
30279
|
+
return {
|
|
30280
|
+
metric,
|
|
30281
|
+
kind,
|
|
30282
|
+
status: "not_reported_by_game",
|
|
30283
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30284
|
+
};
|
|
30285
|
+
}
|
|
30286
|
+
if (timebackValue === undefined) {
|
|
30287
|
+
return {
|
|
30288
|
+
metric,
|
|
30289
|
+
kind,
|
|
30290
|
+
status: "not_recorded_by_timeback",
|
|
30291
|
+
gameValue
|
|
30292
|
+
};
|
|
30293
|
+
}
|
|
30294
|
+
const delta = gameValue - timebackValue;
|
|
30295
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30296
|
+
return {
|
|
30297
|
+
metric,
|
|
30298
|
+
kind,
|
|
30299
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
30300
|
+
timebackValue,
|
|
30301
|
+
gameValue,
|
|
30302
|
+
delta
|
|
30303
|
+
};
|
|
30304
|
+
}
|
|
30305
|
+
function createRunComparison(activity, gameRun) {
|
|
30306
|
+
const runId = activity.runId ?? "";
|
|
30307
|
+
if (!gameRun) {
|
|
30308
|
+
return {
|
|
30309
|
+
runId,
|
|
30310
|
+
status: "not_reported",
|
|
30311
|
+
discrepancyCount: 0,
|
|
30312
|
+
rows: []
|
|
30313
|
+
};
|
|
30314
|
+
}
|
|
30315
|
+
const rows = [
|
|
30316
|
+
createMetricRow({
|
|
30317
|
+
metric: "xp",
|
|
30318
|
+
kind: "number",
|
|
30319
|
+
timebackValue: activity.xpDelta,
|
|
30320
|
+
gameValue: gameRun.totalXp,
|
|
30321
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30322
|
+
}),
|
|
30323
|
+
createMetricRow({
|
|
30324
|
+
metric: "mastery",
|
|
30325
|
+
kind: "number",
|
|
30326
|
+
timebackValue: activity.masteredUnitsDelta,
|
|
30327
|
+
gameValue: gameRun.masteredUnits,
|
|
30328
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30329
|
+
}),
|
|
30330
|
+
createMetricRow({
|
|
30331
|
+
metric: "time",
|
|
30332
|
+
kind: "time",
|
|
30333
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
30334
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
30335
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30336
|
+
}),
|
|
30337
|
+
createMetricRow({
|
|
30338
|
+
metric: "score",
|
|
30339
|
+
kind: "percent",
|
|
30340
|
+
timebackValue: activity.score,
|
|
30341
|
+
gameValue: gameRun.score,
|
|
30342
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30343
|
+
})
|
|
30344
|
+
].filter((row) => row !== null);
|
|
30345
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30346
|
+
return {
|
|
30347
|
+
runId,
|
|
30348
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
30349
|
+
discrepancyCount,
|
|
30350
|
+
rows
|
|
30351
|
+
};
|
|
30352
|
+
}
|
|
30353
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
30354
|
+
return {
|
|
30355
|
+
runId: comparison.runId,
|
|
30356
|
+
status: comparison.status,
|
|
30357
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
30358
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
30359
|
+
};
|
|
30360
|
+
}
|
|
30361
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
30362
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
30363
|
+
const comparisons = new Map;
|
|
30364
|
+
if (activitiesWithRunIds.length === 0) {
|
|
30365
|
+
return comparisons;
|
|
30366
|
+
}
|
|
30367
|
+
if (!response.supported) {
|
|
30368
|
+
for (const activity of activitiesWithRunIds) {
|
|
30369
|
+
comparisons.set(activity.runId, {
|
|
30370
|
+
runId: activity.runId,
|
|
30371
|
+
status: "unavailable",
|
|
30372
|
+
discrepancyCount: 0,
|
|
30373
|
+
reason: response.reason,
|
|
30374
|
+
rows: []
|
|
30375
|
+
});
|
|
30376
|
+
}
|
|
30377
|
+
return comparisons;
|
|
30378
|
+
}
|
|
30379
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
30380
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
30381
|
+
for (const activity of activitiesWithRunIds) {
|
|
30382
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
30383
|
+
}
|
|
30384
|
+
return comparisons;
|
|
30385
|
+
}
|
|
30386
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
30387
|
+
init_src();
|
|
30388
|
+
});
|
|
30389
|
+
|
|
30261
30390
|
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
30262
30391
|
async function upsertMasteryCompletionEntry(params) {
|
|
30263
30392
|
const { client, courseId, studentId, appName, action } = params;
|
|
@@ -30648,6 +30777,8 @@ class TimebackAdminService {
|
|
|
30648
30777
|
static ANALYTICS_CONCURRENCY = 8;
|
|
30649
30778
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
30650
30779
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
30780
|
+
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
30781
|
+
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
30651
30782
|
constructor(deps) {
|
|
30652
30783
|
this.deps = deps;
|
|
30653
30784
|
}
|
|
@@ -30657,13 +30788,42 @@ class TimebackAdminService {
|
|
|
30657
30788
|
}
|
|
30658
30789
|
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
30659
30790
|
}
|
|
30660
|
-
static resolveGameMetricsUrl(baseUrl) {
|
|
30791
|
+
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
30661
30792
|
try {
|
|
30662
|
-
|
|
30793
|
+
const url2 = new URL("/__playcademy/metrics", baseUrl);
|
|
30794
|
+
for (const runId of runIds ?? []) {
|
|
30795
|
+
url2.searchParams.append("runId", runId);
|
|
30796
|
+
}
|
|
30797
|
+
return url2;
|
|
30663
30798
|
} catch {
|
|
30664
30799
|
return null;
|
|
30665
30800
|
}
|
|
30666
30801
|
}
|
|
30802
|
+
static normalizeRunIds(runIds, limit = Number.POSITIVE_INFINITY) {
|
|
30803
|
+
const normalized = [];
|
|
30804
|
+
const seen = new Set;
|
|
30805
|
+
for (const runId of runIds ?? []) {
|
|
30806
|
+
const value = runId.trim().toLowerCase();
|
|
30807
|
+
if (isValidUUID(value) && !seen.has(value)) {
|
|
30808
|
+
seen.add(value);
|
|
30809
|
+
normalized.push(value);
|
|
30810
|
+
if (normalized.length >= limit) {
|
|
30811
|
+
break;
|
|
30812
|
+
}
|
|
30813
|
+
}
|
|
30814
|
+
}
|
|
30815
|
+
return normalized;
|
|
30816
|
+
}
|
|
30817
|
+
static chunkRunIds(runIds) {
|
|
30818
|
+
const chunks = [];
|
|
30819
|
+
for (let index2 = 0;index2 < runIds.length; index2 += this.GAME_METRICS_RUN_IDS_PER_REQUEST) {
|
|
30820
|
+
chunks.push(runIds.slice(index2, index2 + this.GAME_METRICS_RUN_IDS_PER_REQUEST));
|
|
30821
|
+
}
|
|
30822
|
+
return chunks;
|
|
30823
|
+
}
|
|
30824
|
+
static isAbortError(error) {
|
|
30825
|
+
return error instanceof Error && error.name === "AbortError";
|
|
30826
|
+
}
|
|
30667
30827
|
static roundXpToTenths(value) {
|
|
30668
30828
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
30669
30829
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -30824,6 +30984,56 @@ class TimebackAdminService {
|
|
|
30824
30984
|
const remediationItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
|
|
30825
30985
|
return [...groupedGameplayItems, ...remediationItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
30826
30986
|
}
|
|
30987
|
+
async getGameMetricComparisonsForActivities(user, options) {
|
|
30988
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.activities.map((activity) => activity.runId).filter((runId) => Boolean(runId)));
|
|
30989
|
+
if (runIds.length === 0) {
|
|
30990
|
+
return new Map;
|
|
30991
|
+
}
|
|
30992
|
+
const activitiesByRunId = new Map(options.activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0).map((activity) => [activity.runId.toLowerCase(), activity]));
|
|
30993
|
+
const comparisons = new Map;
|
|
30994
|
+
await Promise.all(TimebackAdminService.chunkRunIds(runIds).map(async (chunk) => {
|
|
30995
|
+
const activities = [];
|
|
30996
|
+
for (const runId of chunk) {
|
|
30997
|
+
const activity = activitiesByRunId.get(runId);
|
|
30998
|
+
if (activity) {
|
|
30999
|
+
activities.push(activity);
|
|
31000
|
+
}
|
|
31001
|
+
}
|
|
31002
|
+
if (activities.length === 0) {
|
|
31003
|
+
return;
|
|
31004
|
+
}
|
|
31005
|
+
let response;
|
|
31006
|
+
try {
|
|
31007
|
+
response = await this.getGameMetrics(options.gameId, options.studentId, user, {
|
|
31008
|
+
runIds: chunk,
|
|
31009
|
+
timeoutMs: options.timeoutMs
|
|
31010
|
+
});
|
|
31011
|
+
} catch (error) {
|
|
31012
|
+
response = {
|
|
31013
|
+
supported: false,
|
|
31014
|
+
reason: "fetch_failed",
|
|
31015
|
+
details: error instanceof Error ? error.message : String(error)
|
|
31016
|
+
};
|
|
31017
|
+
}
|
|
31018
|
+
for (const [runId, comparison] of buildGameRunMetricComparisons(activities, options.course, response)) {
|
|
31019
|
+
comparisons.set(runId, comparison);
|
|
31020
|
+
}
|
|
31021
|
+
}));
|
|
31022
|
+
return comparisons;
|
|
31023
|
+
}
|
|
31024
|
+
async attachGameMetricSummariesToActivities(user, options) {
|
|
31025
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
31026
|
+
if (comparisons.size === 0) {
|
|
31027
|
+
return [...options.activities];
|
|
31028
|
+
}
|
|
31029
|
+
return options.activities.map((activity) => {
|
|
31030
|
+
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31031
|
+
return comparison ? {
|
|
31032
|
+
...activity,
|
|
31033
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
31034
|
+
} : activity;
|
|
31035
|
+
});
|
|
31036
|
+
}
|
|
30827
31037
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
30828
31038
|
const enrollments = new Map;
|
|
30829
31039
|
const allEnrollments = new Map;
|
|
@@ -30983,7 +31193,7 @@ class TimebackAdminService {
|
|
|
30983
31193
|
});
|
|
30984
31194
|
return { gameId, courseId, students: deduped };
|
|
30985
31195
|
}
|
|
30986
|
-
async getGameMetrics(gameId, timebackId, user) {
|
|
31196
|
+
async getGameMetrics(gameId, timebackId, user, options) {
|
|
30987
31197
|
const client = this.requireClient();
|
|
30988
31198
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
30989
31199
|
const [targetUser, integrations, game2, deployment] = await Promise.all([
|
|
@@ -31018,7 +31228,8 @@ class TimebackAdminService {
|
|
|
31018
31228
|
if (!metricsBaseUrl) {
|
|
31019
31229
|
return { supported: false, reason: "no_active_deployment" };
|
|
31020
31230
|
}
|
|
31021
|
-
const
|
|
31231
|
+
const runIds = TimebackAdminService.normalizeRunIds(options?.runIds, TimebackAdminService.GAME_METRICS_RUN_IDS_PER_REQUEST);
|
|
31232
|
+
const metricsUrl = TimebackAdminService.resolveGameMetricsUrl(metricsBaseUrl, runIds);
|
|
31022
31233
|
if (!metricsUrl) {
|
|
31023
31234
|
return {
|
|
31024
31235
|
supported: false,
|
|
@@ -31028,7 +31239,7 @@ class TimebackAdminService {
|
|
|
31028
31239
|
}
|
|
31029
31240
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
31030
31241
|
const controller = new AbortController;
|
|
31031
|
-
const timeout = setTimeout(() => controller.abort(), TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31242
|
+
const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31032
31243
|
let response;
|
|
31033
31244
|
try {
|
|
31034
31245
|
response = await fetch(metricsUrl, {
|
|
@@ -31040,10 +31251,19 @@ class TimebackAdminService {
|
|
|
31040
31251
|
signal: controller.signal
|
|
31041
31252
|
});
|
|
31042
31253
|
} catch (error) {
|
|
31254
|
+
const timedOut = TimebackAdminService.isAbortError(error);
|
|
31255
|
+
let details;
|
|
31256
|
+
if (timedOut) {
|
|
31257
|
+
details = "Game metrics request timed out";
|
|
31258
|
+
} else if (error instanceof Error) {
|
|
31259
|
+
details = error.message;
|
|
31260
|
+
} else {
|
|
31261
|
+
details = String(error);
|
|
31262
|
+
}
|
|
31043
31263
|
return {
|
|
31044
31264
|
supported: false,
|
|
31045
|
-
reason: "fetch_failed",
|
|
31046
|
-
details
|
|
31265
|
+
reason: timedOut ? "timeout" : "fetch_failed",
|
|
31266
|
+
details
|
|
31047
31267
|
};
|
|
31048
31268
|
} finally {
|
|
31049
31269
|
clearTimeout(timeout);
|
|
@@ -31147,7 +31367,14 @@ class TimebackAdminService {
|
|
|
31147
31367
|
const allActivities = await this.listRecentActivityForStudent(client, studentId, gameSource, relevantCourseIds, fetchLimit);
|
|
31148
31368
|
const activities = allActivities.slice(safeOffset, safeOffset + safeLimit);
|
|
31149
31369
|
const hasMore = allActivities.length > safeOffset + safeLimit;
|
|
31150
|
-
|
|
31370
|
+
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
31371
|
+
gameId,
|
|
31372
|
+
studentId,
|
|
31373
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31374
|
+
activities,
|
|
31375
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31376
|
+
});
|
|
31377
|
+
return { activities: activitiesWithGameMetrics, hasMore };
|
|
31151
31378
|
}
|
|
31152
31379
|
async getActivityDetail(user, options) {
|
|
31153
31380
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
@@ -31183,7 +31410,22 @@ class TimebackAdminService {
|
|
|
31183
31410
|
if (!activity) {
|
|
31184
31411
|
throw new NotFoundError("Activity", activityId);
|
|
31185
31412
|
}
|
|
31186
|
-
|
|
31413
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
31414
|
+
gameId,
|
|
31415
|
+
studentId,
|
|
31416
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31417
|
+
activities: [activity]
|
|
31418
|
+
});
|
|
31419
|
+
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31420
|
+
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
31421
|
+
...activity,
|
|
31422
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
31423
|
+
} : activity;
|
|
31424
|
+
return {
|
|
31425
|
+
activity: activityWithGameMetrics,
|
|
31426
|
+
rawEvents: matchedEvents,
|
|
31427
|
+
...gameMetricsComparison ? { gameMetricsComparison } : {}
|
|
31428
|
+
};
|
|
31187
31429
|
}
|
|
31188
31430
|
async grantManualXp(data, user) {
|
|
31189
31431
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
@@ -31471,6 +31713,7 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
31471
31713
|
init_errors();
|
|
31472
31714
|
init_timeback_admin_metrics_util();
|
|
31473
31715
|
init_timeback_admin_util();
|
|
31716
|
+
init_timeback_game_metrics_comparison_util();
|
|
31474
31717
|
init_timeback_mastery_completion_util();
|
|
31475
31718
|
init_timeback_util();
|
|
31476
31719
|
logger17 = log.scope("TimebackAdminService");
|
|
@@ -94207,15 +94450,19 @@ var init_timeback_controller = __esm(() => {
|
|
|
94207
94450
|
getGameMetrics = requireGameManagementAccess(async (ctx) => {
|
|
94208
94451
|
const gameId = ctx.params.gameId;
|
|
94209
94452
|
const timebackId = ctx.params.timebackId;
|
|
94453
|
+
const runIds = [
|
|
94454
|
+
...new Set(ctx.url.searchParams.getAll("runId").map((runId) => runId.trim().toLowerCase()).filter(isValidUUID))
|
|
94455
|
+
];
|
|
94210
94456
|
if (!gameId || !timebackId) {
|
|
94211
94457
|
throw ApiError.badRequest("Missing gameId or timebackId path parameter");
|
|
94212
94458
|
}
|
|
94213
94459
|
logger45.debug("Getting game metrics", {
|
|
94214
94460
|
requesterId: ctx.user.id,
|
|
94215
94461
|
gameId,
|
|
94216
|
-
timebackId
|
|
94462
|
+
timebackId,
|
|
94463
|
+
runIds
|
|
94217
94464
|
});
|
|
94218
|
-
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user);
|
|
94465
|
+
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user, { runIds });
|
|
94219
94466
|
});
|
|
94220
94467
|
getStudentActivity = requireGameManagementAccess(async (ctx) => {
|
|
94221
94468
|
const timebackId = ctx.params.timebackId;
|
package/dist/constants.js
CHANGED
|
@@ -78,7 +78,7 @@ var init_platform = __esm(() => {
|
|
|
78
78
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
79
79
|
|
|
80
80
|
// ../constants/src/timeback.ts
|
|
81
|
-
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
81
|
+
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
|
|
82
82
|
var init_timeback = __esm(() => {
|
|
83
83
|
TIMEBACK_ROUTES = {
|
|
84
84
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
@@ -123,6 +123,17 @@ var init_timeback = __esm(() => {
|
|
|
123
123
|
sortOrder: 1,
|
|
124
124
|
lessonType: "quiz"
|
|
125
125
|
};
|
|
126
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
|
|
127
|
+
xp: 1,
|
|
128
|
+
mastery: 0,
|
|
129
|
+
score: 2
|
|
130
|
+
};
|
|
131
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
|
|
132
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
|
|
133
|
+
mastery: 0,
|
|
134
|
+
time: 60,
|
|
135
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
|
|
136
|
+
};
|
|
126
137
|
});
|
|
127
138
|
|
|
128
139
|
// ../constants/src/cloudflare.ts
|
package/dist/server.js
CHANGED
|
@@ -242,7 +242,7 @@ var init_platform = __esm(() => {
|
|
|
242
242
|
var PLATFORM_TIMEZONE = "America/New_York";
|
|
243
243
|
|
|
244
244
|
// ../constants/src/timeback.ts
|
|
245
|
-
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS;
|
|
245
|
+
var TIMEBACK_ROUTES, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
|
|
246
246
|
var init_timeback2 = __esm(() => {
|
|
247
247
|
TIMEBACK_ROUTES = {
|
|
248
248
|
END_ACTIVITY: "/integrations/timeback/end-activity",
|
|
@@ -287,6 +287,17 @@ var init_timeback2 = __esm(() => {
|
|
|
287
287
|
sortOrder: 1,
|
|
288
288
|
lessonType: "quiz"
|
|
289
289
|
};
|
|
290
|
+
TIMEBACK_GAME_METRIC_DECIMAL_PLACES = {
|
|
291
|
+
xp: 1,
|
|
292
|
+
mastery: 0,
|
|
293
|
+
score: 2
|
|
294
|
+
};
|
|
295
|
+
TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE = {
|
|
296
|
+
xp: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.xp,
|
|
297
|
+
mastery: 0,
|
|
298
|
+
time: 60,
|
|
299
|
+
score: 0.5 / 10 ** TIMEBACK_GAME_METRIC_DECIMAL_PLACES.score
|
|
300
|
+
};
|
|
290
301
|
});
|
|
291
302
|
|
|
292
303
|
// ../constants/src/cloudflare.ts
|
|
@@ -1064,7 +1075,7 @@ var package_default;
|
|
|
1064
1075
|
var init_package = __esm(() => {
|
|
1065
1076
|
package_default = {
|
|
1066
1077
|
name: "@playcademy/sandbox",
|
|
1067
|
-
version: "0.4.1-beta.
|
|
1078
|
+
version: "0.4.1-beta.2",
|
|
1068
1079
|
description: "Local development server for Playcademy game development",
|
|
1069
1080
|
type: "module",
|
|
1070
1081
|
exports: {
|
|
@@ -29890,7 +29901,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29890
29901
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29891
29902
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29892
29903
|
}
|
|
29893
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema,
|
|
29904
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29894
29905
|
var init_schemas4 = __esm(() => {
|
|
29895
29906
|
init_drizzle_zod();
|
|
29896
29907
|
init_esm();
|
|
@@ -29965,22 +29976,22 @@ var init_schemas4 = __esm(() => {
|
|
|
29965
29976
|
message: "Cannot provide both masteredUnits and masteredUnitsAbsolute",
|
|
29966
29977
|
path: ["masteredUnitsAbsolute"]
|
|
29967
29978
|
});
|
|
29968
|
-
|
|
29979
|
+
GameRunMetricsSchema = exports_external.object({
|
|
29980
|
+
runId: exports_external.string().uuid(),
|
|
29969
29981
|
activityId: exports_external.string().min(1),
|
|
29970
29982
|
activityName: exports_external.string().optional(),
|
|
29971
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29972
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29973
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29974
|
-
|
|
29975
|
-
lastCompletedAt: exports_external.string().datetime().optional()
|
|
29983
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29984
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29985
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29986
|
+
score: exports_external.number().min(0).max(100).optional()
|
|
29976
29987
|
});
|
|
29977
29988
|
GameCourseMetricsSchema = exports_external.object({
|
|
29978
29989
|
grade: TimebackGradeSchema,
|
|
29979
29990
|
subject: TimebackSubjectSchema,
|
|
29980
|
-
totalXp: exports_external.number().nonnegative(),
|
|
29981
|
-
masteredUnits: exports_external.number().int().nonnegative(),
|
|
29982
|
-
activeTimeSeconds: exports_external.number().nonnegative(),
|
|
29983
|
-
activities: exports_external.array(
|
|
29991
|
+
totalXp: exports_external.number().nonnegative().optional(),
|
|
29992
|
+
masteredUnits: exports_external.number().int().nonnegative().optional(),
|
|
29993
|
+
activeTimeSeconds: exports_external.number().nonnegative().optional(),
|
|
29994
|
+
activities: exports_external.array(GameRunMetricsSchema).optional()
|
|
29984
29995
|
});
|
|
29985
29996
|
GameMetricsResponseSchema = exports_external.object({
|
|
29986
29997
|
studentId: exports_external.string().min(1),
|
|
@@ -30257,6 +30268,124 @@ var init_timeback_admin_util = __esm(() => {
|
|
|
30257
30268
|
init_errors();
|
|
30258
30269
|
});
|
|
30259
30270
|
|
|
30271
|
+
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30272
|
+
function createMetricRow(definition) {
|
|
30273
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30274
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
30275
|
+
return null;
|
|
30276
|
+
}
|
|
30277
|
+
if (gameValue === undefined) {
|
|
30278
|
+
return {
|
|
30279
|
+
metric,
|
|
30280
|
+
kind,
|
|
30281
|
+
status: "not_reported_by_game",
|
|
30282
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30283
|
+
};
|
|
30284
|
+
}
|
|
30285
|
+
if (timebackValue === undefined) {
|
|
30286
|
+
return {
|
|
30287
|
+
metric,
|
|
30288
|
+
kind,
|
|
30289
|
+
status: "not_recorded_by_timeback",
|
|
30290
|
+
gameValue
|
|
30291
|
+
};
|
|
30292
|
+
}
|
|
30293
|
+
const delta = gameValue - timebackValue;
|
|
30294
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30295
|
+
return {
|
|
30296
|
+
metric,
|
|
30297
|
+
kind,
|
|
30298
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
30299
|
+
timebackValue,
|
|
30300
|
+
gameValue,
|
|
30301
|
+
delta
|
|
30302
|
+
};
|
|
30303
|
+
}
|
|
30304
|
+
function createRunComparison(activity, gameRun) {
|
|
30305
|
+
const runId = activity.runId ?? "";
|
|
30306
|
+
if (!gameRun) {
|
|
30307
|
+
return {
|
|
30308
|
+
runId,
|
|
30309
|
+
status: "not_reported",
|
|
30310
|
+
discrepancyCount: 0,
|
|
30311
|
+
rows: []
|
|
30312
|
+
};
|
|
30313
|
+
}
|
|
30314
|
+
const rows = [
|
|
30315
|
+
createMetricRow({
|
|
30316
|
+
metric: "xp",
|
|
30317
|
+
kind: "number",
|
|
30318
|
+
timebackValue: activity.xpDelta,
|
|
30319
|
+
gameValue: gameRun.totalXp,
|
|
30320
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30321
|
+
}),
|
|
30322
|
+
createMetricRow({
|
|
30323
|
+
metric: "mastery",
|
|
30324
|
+
kind: "number",
|
|
30325
|
+
timebackValue: activity.masteredUnitsDelta,
|
|
30326
|
+
gameValue: gameRun.masteredUnits,
|
|
30327
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30328
|
+
}),
|
|
30329
|
+
createMetricRow({
|
|
30330
|
+
metric: "time",
|
|
30331
|
+
kind: "time",
|
|
30332
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
30333
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
30334
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30335
|
+
}),
|
|
30336
|
+
createMetricRow({
|
|
30337
|
+
metric: "score",
|
|
30338
|
+
kind: "percent",
|
|
30339
|
+
timebackValue: activity.score,
|
|
30340
|
+
gameValue: gameRun.score,
|
|
30341
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30342
|
+
})
|
|
30343
|
+
].filter((row) => row !== null);
|
|
30344
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30345
|
+
return {
|
|
30346
|
+
runId,
|
|
30347
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
30348
|
+
discrepancyCount,
|
|
30349
|
+
rows
|
|
30350
|
+
};
|
|
30351
|
+
}
|
|
30352
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
30353
|
+
return {
|
|
30354
|
+
runId: comparison.runId,
|
|
30355
|
+
status: comparison.status,
|
|
30356
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
30357
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
30358
|
+
};
|
|
30359
|
+
}
|
|
30360
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
30361
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
30362
|
+
const comparisons = new Map;
|
|
30363
|
+
if (activitiesWithRunIds.length === 0) {
|
|
30364
|
+
return comparisons;
|
|
30365
|
+
}
|
|
30366
|
+
if (!response.supported) {
|
|
30367
|
+
for (const activity of activitiesWithRunIds) {
|
|
30368
|
+
comparisons.set(activity.runId, {
|
|
30369
|
+
runId: activity.runId,
|
|
30370
|
+
status: "unavailable",
|
|
30371
|
+
discrepancyCount: 0,
|
|
30372
|
+
reason: response.reason,
|
|
30373
|
+
rows: []
|
|
30374
|
+
});
|
|
30375
|
+
}
|
|
30376
|
+
return comparisons;
|
|
30377
|
+
}
|
|
30378
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
30379
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
30380
|
+
for (const activity of activitiesWithRunIds) {
|
|
30381
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
30382
|
+
}
|
|
30383
|
+
return comparisons;
|
|
30384
|
+
}
|
|
30385
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
30386
|
+
init_src();
|
|
30387
|
+
});
|
|
30388
|
+
|
|
30260
30389
|
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
30261
30390
|
async function upsertMasteryCompletionEntry(params) {
|
|
30262
30391
|
const { client, courseId, studentId, appName, action } = params;
|
|
@@ -30647,6 +30776,8 @@ class TimebackAdminService {
|
|
|
30647
30776
|
static ANALYTICS_CONCURRENCY = 8;
|
|
30648
30777
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
30649
30778
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
30779
|
+
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
30780
|
+
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
30650
30781
|
constructor(deps) {
|
|
30651
30782
|
this.deps = deps;
|
|
30652
30783
|
}
|
|
@@ -30656,13 +30787,42 @@ class TimebackAdminService {
|
|
|
30656
30787
|
}
|
|
30657
30788
|
return this.deps.config.localGameUrls[slug2] ?? deployedUrl;
|
|
30658
30789
|
}
|
|
30659
|
-
static resolveGameMetricsUrl(baseUrl) {
|
|
30790
|
+
static resolveGameMetricsUrl(baseUrl, runIds) {
|
|
30660
30791
|
try {
|
|
30661
|
-
|
|
30792
|
+
const url2 = new URL("/__playcademy/metrics", baseUrl);
|
|
30793
|
+
for (const runId of runIds ?? []) {
|
|
30794
|
+
url2.searchParams.append("runId", runId);
|
|
30795
|
+
}
|
|
30796
|
+
return url2;
|
|
30662
30797
|
} catch {
|
|
30663
30798
|
return null;
|
|
30664
30799
|
}
|
|
30665
30800
|
}
|
|
30801
|
+
static normalizeRunIds(runIds, limit = Number.POSITIVE_INFINITY) {
|
|
30802
|
+
const normalized = [];
|
|
30803
|
+
const seen = new Set;
|
|
30804
|
+
for (const runId of runIds ?? []) {
|
|
30805
|
+
const value = runId.trim().toLowerCase();
|
|
30806
|
+
if (isValidUUID(value) && !seen.has(value)) {
|
|
30807
|
+
seen.add(value);
|
|
30808
|
+
normalized.push(value);
|
|
30809
|
+
if (normalized.length >= limit) {
|
|
30810
|
+
break;
|
|
30811
|
+
}
|
|
30812
|
+
}
|
|
30813
|
+
}
|
|
30814
|
+
return normalized;
|
|
30815
|
+
}
|
|
30816
|
+
static chunkRunIds(runIds) {
|
|
30817
|
+
const chunks = [];
|
|
30818
|
+
for (let index2 = 0;index2 < runIds.length; index2 += this.GAME_METRICS_RUN_IDS_PER_REQUEST) {
|
|
30819
|
+
chunks.push(runIds.slice(index2, index2 + this.GAME_METRICS_RUN_IDS_PER_REQUEST));
|
|
30820
|
+
}
|
|
30821
|
+
return chunks;
|
|
30822
|
+
}
|
|
30823
|
+
static isAbortError(error) {
|
|
30824
|
+
return error instanceof Error && error.name === "AbortError";
|
|
30825
|
+
}
|
|
30666
30826
|
static roundXpToTenths(value) {
|
|
30667
30827
|
const rounded = Math.round(value * TimebackAdminService.XP_PRECISION_FACTOR) / TimebackAdminService.XP_PRECISION_FACTOR;
|
|
30668
30828
|
return Object.is(rounded, -0) ? 0 : rounded;
|
|
@@ -30823,6 +30983,56 @@ class TimebackAdminService {
|
|
|
30823
30983
|
const remediationItems = events.map((event) => mapCaliperEventToRemediationActivity(event, relevantCourseIds)).filter((item) => Boolean(item));
|
|
30824
30984
|
return [...groupedGameplayItems, ...remediationItems].toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
30825
30985
|
}
|
|
30986
|
+
async getGameMetricComparisonsForActivities(user, options) {
|
|
30987
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.activities.map((activity) => activity.runId).filter((runId) => Boolean(runId)));
|
|
30988
|
+
if (runIds.length === 0) {
|
|
30989
|
+
return new Map;
|
|
30990
|
+
}
|
|
30991
|
+
const activitiesByRunId = new Map(options.activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0).map((activity) => [activity.runId.toLowerCase(), activity]));
|
|
30992
|
+
const comparisons = new Map;
|
|
30993
|
+
await Promise.all(TimebackAdminService.chunkRunIds(runIds).map(async (chunk) => {
|
|
30994
|
+
const activities = [];
|
|
30995
|
+
for (const runId of chunk) {
|
|
30996
|
+
const activity = activitiesByRunId.get(runId);
|
|
30997
|
+
if (activity) {
|
|
30998
|
+
activities.push(activity);
|
|
30999
|
+
}
|
|
31000
|
+
}
|
|
31001
|
+
if (activities.length === 0) {
|
|
31002
|
+
return;
|
|
31003
|
+
}
|
|
31004
|
+
let response;
|
|
31005
|
+
try {
|
|
31006
|
+
response = await this.getGameMetrics(options.gameId, options.studentId, user, {
|
|
31007
|
+
runIds: chunk,
|
|
31008
|
+
timeoutMs: options.timeoutMs
|
|
31009
|
+
});
|
|
31010
|
+
} catch (error) {
|
|
31011
|
+
response = {
|
|
31012
|
+
supported: false,
|
|
31013
|
+
reason: "fetch_failed",
|
|
31014
|
+
details: error instanceof Error ? error.message : String(error)
|
|
31015
|
+
};
|
|
31016
|
+
}
|
|
31017
|
+
for (const [runId, comparison] of buildGameRunMetricComparisons(activities, options.course, response)) {
|
|
31018
|
+
comparisons.set(runId, comparison);
|
|
31019
|
+
}
|
|
31020
|
+
}));
|
|
31021
|
+
return comparisons;
|
|
31022
|
+
}
|
|
31023
|
+
async attachGameMetricSummariesToActivities(user, options) {
|
|
31024
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
31025
|
+
if (comparisons.size === 0) {
|
|
31026
|
+
return [...options.activities];
|
|
31027
|
+
}
|
|
31028
|
+
return options.activities.map((activity) => {
|
|
31029
|
+
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31030
|
+
return comparison ? {
|
|
31031
|
+
...activity,
|
|
31032
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
31033
|
+
} : activity;
|
|
31034
|
+
});
|
|
31035
|
+
}
|
|
30826
31036
|
async getStudentEnrollmentsByCourseId(client, studentId, courseIds, options) {
|
|
30827
31037
|
const enrollments = new Map;
|
|
30828
31038
|
const allEnrollments = new Map;
|
|
@@ -30982,7 +31192,7 @@ class TimebackAdminService {
|
|
|
30982
31192
|
});
|
|
30983
31193
|
return { gameId, courseId, students: deduped };
|
|
30984
31194
|
}
|
|
30985
|
-
async getGameMetrics(gameId, timebackId, user) {
|
|
31195
|
+
async getGameMetrics(gameId, timebackId, user, options) {
|
|
30986
31196
|
const client = this.requireClient();
|
|
30987
31197
|
await this.deps.validateGameManagementAccess(user, gameId);
|
|
30988
31198
|
const [targetUser, integrations, game2, deployment] = await Promise.all([
|
|
@@ -31017,7 +31227,8 @@ class TimebackAdminService {
|
|
|
31017
31227
|
if (!metricsBaseUrl) {
|
|
31018
31228
|
return { supported: false, reason: "no_active_deployment" };
|
|
31019
31229
|
}
|
|
31020
|
-
const
|
|
31230
|
+
const runIds = TimebackAdminService.normalizeRunIds(options?.runIds, TimebackAdminService.GAME_METRICS_RUN_IDS_PER_REQUEST);
|
|
31231
|
+
const metricsUrl = TimebackAdminService.resolveGameMetricsUrl(metricsBaseUrl, runIds);
|
|
31021
31232
|
if (!metricsUrl) {
|
|
31022
31233
|
return {
|
|
31023
31234
|
supported: false,
|
|
@@ -31027,7 +31238,7 @@ class TimebackAdminService {
|
|
|
31027
31238
|
}
|
|
31028
31239
|
const token = await this.deps.mintPlatformServiceToken(gameId, targetUser.id);
|
|
31029
31240
|
const controller = new AbortController;
|
|
31030
|
-
const timeout = setTimeout(() => controller.abort(), TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31241
|
+
const timeout = setTimeout(() => controller.abort(), options?.timeoutMs ?? TimebackAdminService.GAME_METRICS_FETCH_TIMEOUT_MS);
|
|
31031
31242
|
let response;
|
|
31032
31243
|
try {
|
|
31033
31244
|
response = await fetch(metricsUrl, {
|
|
@@ -31039,10 +31250,19 @@ class TimebackAdminService {
|
|
|
31039
31250
|
signal: controller.signal
|
|
31040
31251
|
});
|
|
31041
31252
|
} catch (error) {
|
|
31253
|
+
const timedOut = TimebackAdminService.isAbortError(error);
|
|
31254
|
+
let details;
|
|
31255
|
+
if (timedOut) {
|
|
31256
|
+
details = "Game metrics request timed out";
|
|
31257
|
+
} else if (error instanceof Error) {
|
|
31258
|
+
details = error.message;
|
|
31259
|
+
} else {
|
|
31260
|
+
details = String(error);
|
|
31261
|
+
}
|
|
31042
31262
|
return {
|
|
31043
31263
|
supported: false,
|
|
31044
|
-
reason: "fetch_failed",
|
|
31045
|
-
details
|
|
31264
|
+
reason: timedOut ? "timeout" : "fetch_failed",
|
|
31265
|
+
details
|
|
31046
31266
|
};
|
|
31047
31267
|
} finally {
|
|
31048
31268
|
clearTimeout(timeout);
|
|
@@ -31146,7 +31366,14 @@ class TimebackAdminService {
|
|
|
31146
31366
|
const allActivities = await this.listRecentActivityForStudent(client, studentId, gameSource, relevantCourseIds, fetchLimit);
|
|
31147
31367
|
const activities = allActivities.slice(safeOffset, safeOffset + safeLimit);
|
|
31148
31368
|
const hasMore = allActivities.length > safeOffset + safeLimit;
|
|
31149
|
-
|
|
31369
|
+
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
31370
|
+
gameId,
|
|
31371
|
+
studentId,
|
|
31372
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31373
|
+
activities,
|
|
31374
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31375
|
+
});
|
|
31376
|
+
return { activities: activitiesWithGameMetrics, hasMore };
|
|
31150
31377
|
}
|
|
31151
31378
|
async getActivityDetail(user, options) {
|
|
31152
31379
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
@@ -31182,7 +31409,22 @@ class TimebackAdminService {
|
|
|
31182
31409
|
if (!activity) {
|
|
31183
31410
|
throw new NotFoundError("Activity", activityId);
|
|
31184
31411
|
}
|
|
31185
|
-
|
|
31412
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
31413
|
+
gameId,
|
|
31414
|
+
studentId,
|
|
31415
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31416
|
+
activities: [activity]
|
|
31417
|
+
});
|
|
31418
|
+
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31419
|
+
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
31420
|
+
...activity,
|
|
31421
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
31422
|
+
} : activity;
|
|
31423
|
+
return {
|
|
31424
|
+
activity: activityWithGameMetrics,
|
|
31425
|
+
rawEvents: matchedEvents,
|
|
31426
|
+
...gameMetricsComparison ? { gameMetricsComparison } : {}
|
|
31427
|
+
};
|
|
31186
31428
|
}
|
|
31187
31429
|
async grantManualXp(data, user) {
|
|
31188
31430
|
const { client, sensorUrl, appName, actor } = await this.resolveAdminMutationContext(data.gameId, data.courseId, user, data.studentId);
|
|
@@ -31470,6 +31712,7 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
31470
31712
|
init_errors();
|
|
31471
31713
|
init_timeback_admin_metrics_util();
|
|
31472
31714
|
init_timeback_admin_util();
|
|
31715
|
+
init_timeback_game_metrics_comparison_util();
|
|
31473
31716
|
init_timeback_mastery_completion_util();
|
|
31474
31717
|
init_timeback_util();
|
|
31475
31718
|
logger17 = log.scope("TimebackAdminService");
|
|
@@ -94206,15 +94449,19 @@ var init_timeback_controller = __esm(() => {
|
|
|
94206
94449
|
getGameMetrics = requireGameManagementAccess(async (ctx) => {
|
|
94207
94450
|
const gameId = ctx.params.gameId;
|
|
94208
94451
|
const timebackId = ctx.params.timebackId;
|
|
94452
|
+
const runIds = [
|
|
94453
|
+
...new Set(ctx.url.searchParams.getAll("runId").map((runId) => runId.trim().toLowerCase()).filter(isValidUUID))
|
|
94454
|
+
];
|
|
94209
94455
|
if (!gameId || !timebackId) {
|
|
94210
94456
|
throw ApiError.badRequest("Missing gameId or timebackId path parameter");
|
|
94211
94457
|
}
|
|
94212
94458
|
logger45.debug("Getting game metrics", {
|
|
94213
94459
|
requesterId: ctx.user.id,
|
|
94214
94460
|
gameId,
|
|
94215
|
-
timebackId
|
|
94461
|
+
timebackId,
|
|
94462
|
+
runIds
|
|
94216
94463
|
});
|
|
94217
|
-
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user);
|
|
94464
|
+
return ctx.services.timebackAdmin.getGameMetrics(gameId, timebackId, ctx.user, { runIds });
|
|
94218
94465
|
});
|
|
94219
94466
|
getStudentActivity = requireGameManagementAccess(async (ctx) => {
|
|
94220
94467
|
const timebackId = ctx.params.timebackId;
|