Skip to content
Merged
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
6 changes: 6 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,11 @@ export default [
'URLSearchParams'
]
}
},
{
// Browser-compat checks apply to shipped library code (src), not tests,
// which run in Node + Vitest's Chromium.
files: ['tests/**/*.ts'],
Comment thread
devandrepascoa marked this conversation as resolved.
rules: { 'compat/compat': 'off' }
}
];
31 changes: 25 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ import PacketLossEngine from './engines/PacketLossEngine';
import ReachabilityEngine from './engines/ReachabilityEngine';

import Results from './Results';
import logFinalResults from './logging/logFinalResults';
import logFinalResults, {
type AimLogResponse
Comment thread
devandrepascoa marked this conversation as resolved.
Comment thread
devandrepascoa marked this conversation as resolved.
} from './logging/logFinalResults';

const DEFAULT_OPTIMAL_DOWNLOAD_SIZE = 1e6;
const DEFAULT_OPTIMAL_UPLOAD_SIZE = 1e6;
Expand Down Expand Up @@ -704,6 +706,7 @@ class MeasurementEngine {
*
* const engine = new SpeedTest();
* engine.onFinish = results => console.log(results.getScores());
* engine.onResultsLogged = ({ requestId }) => console.log('Logged as', requestId);
* ```
*/
class SpeedTestEngine extends MeasurementEngine {
Expand Down Expand Up @@ -737,23 +740,39 @@ class SpeedTestEngine extends MeasurementEngine {
};
}

/**
* Called after the final results have been logged to the AIM API, with the
* endpoint's parsed response (e.g. the assigned `requestId`).
*
* Fires whenever logging is attempted (`logAimApiUrl` is set), after the log
* request completes — on success or failure. On failure, `requestId` is
* `undefined`. Because the log request completes after {@link onFinish}, this
* callback runs slightly later.
Comment thread
devandrepascoa marked this conversation as resolved.
*/
onResultsLogged: (response: AimLogResponse) => void = () => {};

// Internal state
readonly #logAimApiUrl: string | null;
readonly #sessionId: string | undefined;

// Internal methods
#logFinalResults = (results: Results): void => {
this.#logAimApiUrl &&
logFinalResults(results, {
apiUrl: this.#logAimApiUrl,
sessionId: this.#sessionId
});
if (!this.#logAimApiUrl) {
return;
}
logFinalResults(results, {
apiUrl: this.#logAimApiUrl,
sessionId: this.#sessionId
}).then(response => {
Comment thread
devandrepascoa marked this conversation as resolved.
Comment thread
devandrepascoa marked this conversation as resolved.
this.onResultsLogged(response);
});
Comment thread
devandrepascoa marked this conversation as resolved.
};
}

export default SpeedTestEngine;

export type { MeasurementType, PhaseChangePayload };
export type { AimLogResponse };
export { type default as Results } from './Results';
export type {
BandwidthPoint,
Expand Down
32 changes: 25 additions & 7 deletions src/logging/logFinalResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ interface LogData {
[key: string]: unknown;
}

/** Response returned by the AIM logging endpoint. */
export interface AimLogResponse {
/** Unique identifier assigned to this measurement, used to look it up later. */
requestId?: string;
}

type ParserFn = (val: unknown) => unknown;

/** Rounds a number to a given number of decimal places. */
Expand Down Expand Up @@ -91,12 +97,16 @@ const scoreParser = (

/**
* Formats measurement results and AIM scores, then POSTs them to the
* AIM logging endpoint. Fire-and-forget — errors are silently ignored.
* AIM logging endpoint. Best-effort — never throws.
*
* Always resolves with an {@link AimLogResponse}: the parsed response body on
* success (e.g. the assigned `requestId`), or `{ requestId: undefined }` if the
* request failed or returned no usable body.
*/
const logAimResults = (
const logAimResults = async (
results: Results,
{ apiUrl, sessionId }: LogConfig
): void => {
): Promise<AimLogResponse> => {
const logData: LogData = {
sessionId
};
Expand All @@ -118,10 +128,18 @@ const logAimResults = (
);
}

fetch(apiUrl, {
method: 'POST',
body: JSON.stringify(logData)
});
try {
const response = await fetch(apiUrl, {
method: 'POST',
body: JSON.stringify(logData)
});
if (!response.ok) {
return { requestId: undefined };
}
return (await response.json()) as AimLogResponse;
Comment thread
devandrepascoa marked this conversation as resolved.
Comment thread
devandrepascoa marked this conversation as resolved.
Comment thread
devandrepascoa marked this conversation as resolved.
} catch {
return { requestId: undefined };
}
};

export default logAimResults;
Loading