SPLap 2.1.1 · engineering notes
SPLap canonical data schema (splap.canonical/1)
This document describes the event schema that every SPLap data path produces: the Python CLI
(tools/splapdata/*, tools/splap_import.py), the browser importer
(splap/appserver/static/js/importer.js), offline bundles (bundles/*) and the packaged
lookups (splap/lookups/splap_monza_2025_*.csv.gz). It is written from the code; where the
code and this document disagree, the code wins and this document should be fixed.
Source of truth for names and values: tools/splapdata/canonical.py.
1. Design goals
| Goal | How it is met |
|---|---|
| Provider-agnostic | Adapters (openf1.py, providers_extra.py, importer.js) normalise raw provider payloads into the same event shapes. The app never reads provider response shapes directly. |
| Provenance on every event | Every event carries provenance with one of four values: observed, derived, modeled, unavailable (PROVENANCE tuple in canonical.py; base_event asserts on it). |
| Idempotent imports | Every import has a deterministic import_id (below). Ingest refuses to re-send an import_id that already has a splap:session event in the index unless --force is given. |
| One index | Default index splap (CLI --index, browser importer index field). |
| One sourcetype per event type | SOURCETYPES map: event_type x is ingested as sourcetype splap:x. 17 sourcetypes, all defined in splap/default/props.conf. |
| Never fabricate | Replay frames outside location coverage carry coverage=false and x/y = null; telemetry older than 3 s is marked unavailable, never rendered as zero. |
1.1 import_id
basis = json.dumps({"p": provider, "s": str(session_key), "profile": profile, "w": window or None}, sort_keys=True)
import_id = "imp_" + hashlib.sha256(basis.encode()).hexdigest()[:16]
provider:"openf1"(or"fastf1").session_key: provider session key, always stringified ("9912").profile: one ofquick,engineering,selected,full(CLI--profile, defaultfull).window:[iso_start, iso_end]list ornull. The window strings are used exactly as passed (the shipped Monza window is["2025-09-07T14:00:00", "2025-09-07T14:13:30"]).
Because sort_keys=True orders the keys p, profile, s, w and Python's default separators are
", " and ": ", the exact hashed string for the shipped session is:
{"p": "openf1", "profile": "full", "s": "9912", "w": ["2025-09-07T14:00:00", "2025-09-07T14:13:30"]}
which gives imp_17f4a807fa72146b (see splap/appserver/static/data/sessions.json and the bundle
manifest.json). importer.js (I.importId) rebuilds this string byte-for-byte, including the
Python spacing, so a browser import and a CLI import of the same session+profile+window collide on
purpose. Note: I.sha256 falls back to a non-SHA hash when window.crypto.subtle is unavailable
(non-secure context, i.e. plain http://); in that case browser and CLI ids differ.
1.2 Time handling
| Field | Meaning |
|---|---|
timestamp_utc |
ISO-8601 UTC with millisecond precision and a trailing Z: 2025-09-07T14:00:00.234Z (epoch_to_iso). |
elapsed_ms |
Integer milliseconds since session t0 (= provider date_start). The shipped window starts at elapsed_ms=3600234 because the race session record begins at 13:00Z. |
_time (Splunk) |
Extracted from timestamp_utc via props.conf: TIME_PREFIX = "timestamp_utc":", TIME_FORMAT = %Y-%m-%dT%H:%M:%S.%3NZ, MAX_TIMESTAMP_LOOKAHEAD = 30. Over HEC the CLI also sends an explicit time epoch. |
Provider timestamps such as date_start / date_end on session and meeting events are passed through
in the provider's own format (2025-09-07T13:00:00+00:00), not re-serialised.
2. Common fields (every event)
Produced by base_event() in canonical.py. Adapters add per-type fields on top.
| Field | Type | Notes |
|---|---|---|
event_type |
string | Key into SOURCETYPES. |
source_provider |
string | openf1, fastf1. |
source_system |
string | Same value as source_provider. |
schema_version |
string | splap.canonical/1. |
meeting_key |
int | Provider meeting key (OpenF1 meeting_key, e.g. 1268). |
session_key |
int | Provider session key (e.g. 9912). |
season |
int | OpenF1 year. |
circuit_key |
int | OpenF1 circuit_key (39 = Monza). |
circuit_name |
string | OpenF1 circuit_short_name. |
session_type |
string | OpenF1 session_type (Race, Qualifying, Practice). |
timestamp_utc |
string | See 1.2. |
elapsed_ms |
int | See 1.2. |
provenance |
string | observed / derived / modeled / unavailable. |
import_id |
string | Added to every row at ingest time (ingest_bundle, I.run). Present on the session event from normalisation. |
source_record_id |
string | Adapter-specific stable id of the source row (lap:1:41, pos:<date>:<driver>, ...). Not on location-derived replay frames. |
Driver-scoped events (lap, stint, pit, position, interval, overtake, team_radio) also carry the
drv() block: driver_number, driver_code, driver_name, team_id, team_name,
team_primary_color. location, car_data and race_control carry only driver_number.
3. Sourcetypes
All values are observed unless stated. "Origin" is the OpenF1 endpoint and raw field (openf1.normalise).
splap:meeting (one per import)
| Field | Type | Origin |
|---|---|---|
source_record_id |
string | str(meeting_key) |
meeting_name, meeting_official_name |
string | /meetings |
location, country_name |
string | /meetings |
date_start |
string | /meetings.date_start (Python only; the browser importer omits it) |
Timestamp = session date_start.
splap:session (one per import)
| Field | Type | Origin |
|---|---|---|
source_record_id |
string | str(session_key) |
session_name |
string | /sessions.session_name (Race) |
date_start, date_end |
string | /sessions |
meeting_name, location, country_name, circuit_short_name, gmt_offset |
string | /sessions, /meetings |
import_id, data_mode |
string | data_mode is historical for provider imports, synthetic for Demo Race 07 |
window_start, window_end |
string or null | The import window as passed on the command line |
capabilities |
object | See section 4 |
counts |
object | Event counts per channel key (laps not lap) |
splap:driver (one per driver)
| Field | Type | Origin |
|---|---|---|
source_record_id |
string | "<session_key>:<driver_number>" |
driver_number |
int | /drivers.driver_number |
driver_code |
string | name_acronym (VER) |
driver_name, broadcast_name, first_name, last_name |
string | /drivers |
team_id |
string | team_name lower-cased, spaces to _ |
team_name |
string | /drivers.team_name |
team_primary_color |
string | "#" + team_colour (6-hex) |
country_code |
string | /drivers |
headshot_url |
null | Deliberately not populated (rights) |
splap:circuit (one per import, provenance=derived)
Built at ingest (ingest_bundle, I.run), not by the provider adapter. Timestamp = session date_start.
| Field | Type | Notes |
|---|---|---|
length_m |
float | Derived centerline length |
corners |
int | Number of derived corners |
sectors |
object | {"boundaries_m": [0.0, s1, s2], "provenance": ...} |
pit_lane_provenance |
string | derived or unavailable |
drs_zones |
int | Count of derived DRS zones |
geometry |
string | JSON-encoded geometry object (section 5), stored as a string so KV_MODE does not explode it into thousands of fields |
splap:location (~3.7 Hz per car, decimetres)
| Field | Type/unit | Origin |
|---|---|---|
driver_number |
int | /location |
x, y, z |
number, decimetres (0.1 m) in the venue frame | /location.x/y/z |
splap:car_data (~3.7 Hz per car)
| Field | Type/unit | Origin |
|---|---|---|
driver_number |
int | /car_data |
speed_kph |
int, km/h | speed |
rpm |
int | rpm |
gear |
int 0–8 | n_gear |
throttle_pct |
int 0–100 | throttle |
brake_state |
int (0 / 100 as delivered by OpenF1) | brake |
drs |
int code | drs. Decoding (DRS_STATES): 0,1 = off; 8 = eligible; 10,12,14 = on; other = unknown; null = unavailable |
splap:lap
| Field | Type/unit | Origin |
|---|---|---|
lap_number |
int | /laps |
lap_duration_s |
float s or null | lap_duration |
sector_1_s, sector_2_s, sector_3_s |
float s | duration_sector_1..3 |
i1_speed, i2_speed, st_speed |
int km/h | speed traps |
is_pit_out_lap |
bool | is_pit_out_lap |
Timestamp = date_start of the lap; laps without date_start are dropped.
splap:position
| Field | Type | Origin |
|---|---|---|
position |
int | /position.position |
Fetched for the whole session regardless of window (positions are sparse change events).
splap:interval
| Field | Type/unit | Origin |
|---|---|---|
gap_to_leader |
float s or null | /intervals.gap_to_leader (null when the provider gives a string) |
interval |
float s or null | /intervals.interval |
laps_behind |
int | Parsed from string gaps such as +1 LAP (1 if unparsable, 0 otherwise) |
gap_to_leader_text |
string or null | The raw string when non-numeric |
Fetched from 2 minutes before the window start so every car has a value at the first frame.
splap:pit
| Field | Type/unit | Origin |
|---|---|---|
lap_number |
int | /pit |
pit_duration, lane_duration, stop_duration |
float s or null | /pit |
splap:stint
| Field | Type | Origin |
|---|---|---|
stint_number, lap_start, lap_end |
int | /stints |
compound |
string (SOFT, MEDIUM, HARD, ...) |
/stints.compound |
tyre_age_at_start |
int laps | /stints |
Timestamp = date_start of lap lap_start for that driver, else session start.
splap:weather
| Field | Unit | Origin |
|---|---|---|
air_temperature, track_temperature |
°C | /weather |
humidity |
% | |
pressure |
mbar | |
rainfall |
0/1 | |
wind_speed |
m/s | |
wind_direction |
degrees |
splap:race_control
| Field | Type | Origin |
|---|---|---|
race_control_category |
string | category (Flag, SafetyCar, Drs, ...) |
flag |
string or null | flag (GREEN, YELLOW, CLEAR, ...) |
scope |
string or null | Track, Sector, Driver |
sector |
int or null | Marshal sector |
lap_number |
int or null | |
race_control_message |
string | message |
driver_number |
int or null |
splap:overtake
| Field | Type | Origin |
|---|---|---|
driver_number (+ drv block) |
int | overtaking_driver_number |
overtaken_driver_number |
int | /overtakes |
overtaken_driver_code |
string | Looked up from drivers |
position |
int | Position gained |
splap:team_radio
| Field | Type | Origin |
|---|---|---|
recording_url |
string | /team_radio.recording_url (an OpenF1/F1 URL; audio is not bundled) |
splap:replay_frame (provenance=derived, 500 ms step)
One event per car per frame, only where coverage is true. Built at ingest from the compact
frame payload (section 6). Fields: driver_number, x, y, dist_m, heading, speed_kph,
gear, throttle_pct, brake_state, drs, position, gap_to_leader, interval,
lap_number, compound (single letter), tyre_age, pit (0/1), pit_progress (0–1 or null).
x/y are decimetres; heading radians; dist_m metres along the derived centerline.
Not present on this sourcetype: schema_version, circuit_key, session_type, source_record_id.
splap:import_status
Written by the ingest layer at start and complete. Fields: import_id, phase, detail,
counts (Python), browser (browser importer, truncated user agent), plus session identifiers.
timestamp_utc is wall-clock time of the import, not race time.
4. Capability object
capability_object(counts, extra) in canonical.py; stored on session.capabilities, the
splap:session event, manifest.json and sessions.json.
{
"location": {"available": true, "count": 61800, "provenance": "observed"},
"car_data": {"available": true, "count": 60500, "provenance": "observed"},
"laps": {"available": true, "count": 976, "provenance": "observed"},
"position": {...}, "interval": {...}, "pit": {...}, "stint": {...}, "weather": {...},
"race_control": {...}, "overtake": {...}, "team_radio": {...},
"drs": {"available": true, "count": 60500, "provenance": "observed"},
"aero": {"available": false, "count": 0, "provenance": "unavailable"},
"telemetry_hz": 3.7,
"location_hz": 3.7
}
Channel keys are CHANNEL_KEYS. A channel with zero rows is available=false, provenance=unavailable.
drs is available only if at least one car_data row has a non-null drs. aero is always
unavailable (no provider supplies it). The two _hz values are nominal constants, not measured.
5. Circuit geometry (splap.track/1)
Produced by track.derive(); shipped as splap/appserver/static/sessions/<id>/track.json,
bundles/<id>/track.geojson (splap_track member plus GeoJSON features) and as the JSON string
in splap:circuit.geometry (subset of keys). All coordinates are decimetres (units: "dm",
unit_m: 0.1) in the provider venue frame; distances with _m are metres.
| Key | Type | Provenance | Notes |
|---|---|---|---|
schema, units, unit_m, step_m |
step_m = 5.0 resample step |
||
length_m |
float | derived | Lap length of the centerline (Monza: 5745.0) |
centerline |
[[x,y],...] |
derived | Closed polyline, index 0 = start/finish, increasing index = direction of travel. Built from the fastest clean lap, averaged over up to 6 clean laps, smoothed (2 passes, k=2) and resampled |
cum_dist_m |
[float] |
derived | Cumulative distance per centerline point |
heading |
[rad] |
derived | Central-difference heading |
curvature |
[rad/m] |
derived | 7-point mean of heading rate. Not in the splap:circuit subset |
width_m |
12.0 | modeled | width_provenance: "modeled" constant |
bounds |
{xmin,xmax,ymin,ymax} |
Includes pit lane | |
start_finish |
{dist_m: 0, x, y, provenance} |
derived | Position of the reference driver at lap date_start |
sectors |
{boundaries_m: [0, s1, s2], provenance} |
derived | From the reference lap's sector durations; unavailable if fewer than 3 |
corners |
list | derived | Curvature peaks with abs(curvature) > 1/340 per m, at least 20 m long, split on direction change. Each: number, dist_m, x, y, direction (L/R), radius_m, start_dist_m, end_dist_m |
pit_lane |
{path, length_m, entry_dist_m, exit_dist_m, box, provenance, source_pit_event} |
derived | Samples > 6 m from the centerline during the pit event window with the most off-line points, resampled at 3 m. provenance: unavailable and empty path if no usable pit window |
drs_zones |
[{start_dist_m, end_dist_m}] |
derived (drs_provenance) |
Histogram of car_data.drs in {10,12,14} projected to centerline index; zones longer than 150 m |
reference |
{driver_number, lap_number, laps_averaged, samples} |
Which lap the centerline came from | |
provenance |
derived |
The splap:circuit.geometry string contains: centerline, cum_dist_m, heading, pit_lane,
corners, sectors, drs_zones, start_finish, bounds, length_m, width_m, units.
6. Replay frames (splap.replay.frames/1)
frames.build_frames() samples every car every step_ms = 500 ms between the first and last
location sample (or the requested window). frames.compact() turns that into a column-oriented
payload used by the browser (replay_frames.json.gz in bundles, static/sessions/<id>/replay_frames.json).
{"schema": "splap.replay.frames/1", "step_ms": 500, "n": 1620, "t0_elapsed_ms": 3600234,
"timestamp_utc_0": "2025-09-07T14:00:00.234Z", "drivers": [1, 4, ...],
"cols": ["x","y","d","hd","v","g","th","br","drs","pos","gap","int","lap","cmp","age","pit","pp","cov"],
"cars": {"1": {"x": [...], "y": [...], ...}}}
| Col | Meaning | Source |
|---|---|---|
x, y |
Position, decimetres; linear interpolation between the two bracketing location samples. If the bracketing gap exceeds 5 s the last sample is held (no sweep) | location |
d |
dist_m along centerline (projection) |
derived |
hd |
Heading, radians (from the sample pair, else centerline heading) | derived |
v, g, th, br, drs |
speed_kph, gear, throttle_pct, brake_state, drs from the last car_data row, only if it is less than 3 s old, else null |
car_data |
pos |
Position from the last position row (held from before the window). If missing for a covered car, derived from race distance (lap_number * length + dist_m); the row form marks this position_provenance: "derived" |
position / derived |
gap, int |
gap_to_leader, interval from the last interval row |
interval |
lap |
Current lap number (last lap date_start <= t) |
lap |
cmp |
First letter of stint compound (S, M, H, I, W) |
stint |
age |
tyre_age_at_start + lap_number - lap_start |
stint + lap |
pit |
1 inside a pit window (±0.75 × lane_duration) or when nearer the pit-lane path than the centerline | pit + geometry |
pp |
Pit-lane progress 0–1 along the derived pit path, else null | geometry |
cov |
1 = inside location coverage for this car; 0 = no data (all other columns null) | location |
Coverage semantics: cov=0 means "we do not know where the car is". Renderers must hide the
car or show a placeholder; they must not draw it at (0,0) or at its last position as if live.
Null telemetry columns with cov=1 mean "position known, telemetry stale/unavailable" and the UI
shows TELEMETRY UNAVAILABLE rather than 0 km/h.
The expanded row form (one row per car per frame) is what splap:replay_frame and the
splap_monza_2025_frames lookup contain. The lookup additionally carries driver_code,
laps_behind, stint_number, telemetry (observed/unavailable) and position_provenance;
the indexed sourcetype does not.
7. Splunk-side conventions
- All 17 stanzas in
props.confare identical apart from the name:SHOULD_LINEMERGE=false,LINE_BREAKER=([\r\n]+),KV_MODE=json,TRUNCATE=0, the TIME_* settings from 1.2. - No indexed fields beyond defaults; everything is search-time JSON extraction.
sourceissplap:<provider>(CLI) orsplap:openf1/splap:browser(browser importer). - Field names are stable across sourcetypes, so
driver_number,lap_number,elapsed_msandimport_idcan be used to join across them.
Example SPL (field names as indexed):
# laps per driver, best lap
index=splap sourcetype=splap:lap import_id=imp_17f4a807fa72146b
| stats count AS laps min(lap_duration_s) AS best_s by driver_number driver_code team_name
| sort driver_code
# pit stops with durations
index=splap sourcetype=splap:pit
| table timestamp_utc elapsed_ms driver_code lap_number lane_duration stop_duration pit_duration
| sort elapsed_ms
# running order at a moment in race time (last known position per driver before t)
index=splap sourcetype=splap:position elapsed_ms<=3900000
| stats latest(position) AS position by driver_number driver_code
| sort position
# replay frames for a 60 s window
index=splap sourcetype=splap:replay_frame elapsed_ms>=3600000 elapsed_ms<3660000
| table elapsed_ms driver_number x y dist_m speed_kph position lap_number pit
# what did an import contain?
index=splap import_id=imp_17f4a807fa72146b | stats count by sourcetype
# capability check without reading the session JSON
index=splap sourcetype=splap:session | spath capabilities.car_data.available
elapsed_ms is race-relative; _time is wall clock. Both work for windows; elapsed_ms is what
the UI and macros use so a window is portable across imports of the same session.
8. Packaged (lookup-backed) form
The app ships one real session as gzip-CSV lookups so it works with no index and no network.
tools/splap_import.py fixture writes them and the static assets; session_backend.js
(LookupBackend) reads them. Lookup names come from
splap/appserver/static/sessions/monza_2025/session.json (lookups object); stanzas are in
splap/default/transforms.conf.
| Lookup | Contents | Rows (Monza) | Notes |
|---|---|---|---|
splap_monza_2025_events |
Every event type except location and car_data: session, meeting, driver, lap, stint, pit, position, interval, overtake, race_control, weather, team_radio |
6090 | One wide CSV; columns are the union of all fields, event_type first. Nested values are JSON strings. |
splap_monza_2025_telemetry |
location and car_data rows |
122300 | Fixed columns: event_type, timestamp_utc, elapsed_ms, driver_number, x, y, z, speed_kph, rpm, gear, throttle_pct, brake_state, drs, provenance, session_key |
splap_monza_2025_frames |
Expanded replay frame rows (covered cars only) | 32400 | Columns listed in section 6 |
Field names are identical to the indexed form, so | inputlookup splap_monza_2025_events | where event_type="lap"
is equivalent to index=splap sourcetype=splap:lap for that import. Circuit geometry, capabilities,
palettes, validation results and attribution for the packaged session come from the static
session.json / track.json / replay_frames.json, not from the lookups. The registry
splap/appserver/static/data/sessions.json (splap.sessions/1) lists packaged sessions with
id, label, session_key, import_id, window, coverage, frames, step_ms,
capabilities, counts, data_mode, provider, default.
The splap_demo_* lookups (Demo Race 07) are a separate, synthetic schema and are not covered here.
9. Validation checks (canonical.Validator)
Run by splap_import.py validate and during fetch; results go to validation.json in the
bundle and into the static session.json. Each result is {check, status, detail} with status
pass, fail or warn. Validator.ok is true when nothing is fail.
| Check | Level | Rule |
|---|---|---|
unique_driver_numbers |
fail | No duplicate driver_number |
driver_team_mapping |
fail | Every driver has team_name |
hex_team_colors |
fail | team_primary_color is 6 hex digits |
monotonic_lap_progression |
fail | Per driver, lap numbers form a contiguous range |
plausible_speed_gear |
fail | 0 <= speed_kph <= 380, 0 <= gear <= 8 on all car_data |
unique_positions_per_frame |
fail | No two cars share a position in a frame |
nondecreasing_gap_to_leader |
warn | Unlapped cars ordered by position have non-decreasing gap, 1 s slack; tolerated on up to 5% of frames |
no_large_coordinate_jumps |
warn | No car moves > 120 m between consecutive 500 ms frames outside the pit |
replay_frame_continuity |
fail | Max gap between frames <= 1500 ms |
timestamps_within_session |
fail | All event timestamps in [t0 - 1 h, t1 + 30 min] |
coordinates_near_track_or_pitlane |
fail | > 97% of sampled frame positions within 40 m of centerline or pit path (only when a track is supplied) |
pit_events_match_pitlane |
warn | Currently always passes; reports the pit event count only |
The shipped Monza window passes 11 checks and warns on nondecreasing_gap_to_leader
(214 of 1620 frames, explained by staggered per-driver interval updates).
