SPLap

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]

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

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).