Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/common/mock-objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,7 @@ export const mockMeasurementModel = [
upload_failed: false,
scheduled_slot: null,
scheduled_at: null,
server_timestamp: null,
device_name: null,
device_model: null,
device_manufacturer: null,
Expand Down Expand Up @@ -685,6 +686,7 @@ export const mockMeasurementModel = [
upload_failed: false,
scheduled_slot: null,
scheduled_at: null,
server_timestamp: null,
device_name: null,
device_model: null,
device_manufacturer: null,
Expand Down Expand Up @@ -735,6 +737,7 @@ export const mockMeasurementModel = [
upload_failed: false,
scheduled_slot: null,
scheduled_at: null,
server_timestamp: null,
device_name: null,
device_model: null,
device_manufacturer: null,
Expand Down
7 changes: 7 additions & 0 deletions src/measurement/measurement.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,13 @@ export class MeasurementDto {
})
scheduled_at?: Date;

@ApiProperty({
required: false,
description:
"M-Lab's own clock as reported by ndt7, independent of the device clock. Null when the client could not read it",
})
server_timestamp?: Date;

@ApiProperty({ required: false, description: 'Device name' })
device_name?: string;

Expand Down
44 changes: 44 additions & 0 deletions src/measurement/measurement.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,50 @@ describe('MeasurementService', () => {
});
});

it('should persist the server timestamp reported by ndt7', async () => {
jest
.spyOn(prisma.dailycheckapp_school, 'findFirst')
.mockResolvedValue(mockSchoolModel[0]);
jest
.spyOn(prisma.giga_id_school_mapping_fix, 'findFirst')
.mockResolvedValue(null);
const createSpy = jest
.spyOn(prisma.measurements, 'create')
.mockResolvedValue(mockMeasurementModel[0]);

const serverTimestamp = new Date('2026-08-24T10:30:00.000Z');
await service.createMeasurement({
...mockAddMeasurementDto[0],
server_timestamp: serverTimestamp,
});

expect(createSpy).toHaveBeenCalledWith({
data: expect.objectContaining({
server_timestamp: serverTimestamp,
}),
});
});

it('should store a null server timestamp when the app omits it', async () => {
jest
.spyOn(prisma.dailycheckapp_school, 'findFirst')
.mockResolvedValue(mockSchoolModel[0]);
jest
.spyOn(prisma.giga_id_school_mapping_fix, 'findFirst')
.mockResolvedValue(null);
const createSpy = jest
.spyOn(prisma.measurements, 'create')
.mockResolvedValue(mockMeasurementModel[0]);

await service.createMeasurement(mockAddMeasurementDto[0]);

expect(createSpy).toHaveBeenCalledWith({
data: expect.objectContaining({
server_timestamp: null,
}),
});
});

it('should persist cloudflare protocol and derived quality metrics', async () => {
jest
.spyOn(prisma.dailycheckapp_school, 'findFirst')
Expand Down
2 changes: 2 additions & 0 deletions src/measurement/measurement.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ export class MeasurementService {
upload_failed: measurement.upload_failed,
scheduled_slot: measurement.scheduled_slot,
scheduled_at: measurement.scheduled_at,
server_timestamp: measurement.server_timestamp,
device_name: measurement.device_name,
device_model: measurement.device_model,
device_manufacturer: measurement.device_manufacturer,
Expand Down Expand Up @@ -659,6 +660,7 @@ export class MeasurementService {
upload_failed: measurement.upload_failed ?? false,
scheduled_slot: measurement.scheduled_slot ?? null,
scheduled_at: measurement.scheduled_at ?? null,
server_timestamp: measurement.server_timestamp ?? null,
device_name: measurement.device_name ?? null,
device_model: measurement.device_model ?? null,
device_manufacturer: measurement.device_manufacturer ?? null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- AlterTable
-- server_timestamp: wall-clock time reported by M-Lab, captured by the ndt7
-- client from the Date header of the locate service response. The existing
-- `timestamp` column comes from the client machine's own clock, which on these
-- devices is often wrong; this is a second, independent reference point rather
-- than a replacement. Null for clients that do not send it, which is every
-- release before app v2.0.4.
ALTER TABLE "measurements" ADD COLUMN "server_timestamp" TIMESTAMPTZ(6);
1 change: 1 addition & 0 deletions src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ model measurements {
upload_failed Boolean? @default(false) // true = realtime upload failed, record arrived via offline sync
scheduled_slot String? @db.VarChar(16) // 'A' | 'B' | 'C' | 'startup'; null for manual runs
scheduled_at DateTime? @db.Timestamptz(6) // originally planned run time of the scheduled test
server_timestamp DateTime? @db.Timestamptz(6) // M-Lab's own clock, reported by ndt7; null when the client could not read it
device_name String?
device_model String?
device_manufacturer String?
Expand Down