arrayview 0.41.1__tar.gz → 0.42.1__tar.gz

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.
Files changed (112) hide show
  1. {arrayview-0.41.1 → arrayview-0.42.1}/PKG-INFO +1 -1
  2. {arrayview-0.41.1 → arrayview-0.42.1}/pyproject.toml +1 -1
  3. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_diagnostics.py +5 -1
  4. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_launch_trace.py +59 -2
  5. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_launcher.py +41 -0
  6. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_query.py +32 -8
  7. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_server.py +22 -0
  8. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_viewer.html +449 -102
  9. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_vscode_extension.py +169 -1
  10. arrayview-0.42.1/src/arrayview/arrayview-opener.vsix +0 -0
  11. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_launch_diagnostics.py +11 -0
  12. arrayview-0.42.1/tests/test_launch_trace.py +231 -0
  13. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_lifecycle_contract.py +21 -6
  14. arrayview-0.41.1/src/arrayview/arrayview-opener.vsix +0 -0
  15. arrayview-0.41.1/tests/test_launch_trace.py +0 -99
  16. {arrayview-0.41.1 → arrayview-0.42.1}/.gitignore +0 -0
  17. {arrayview-0.41.1 → arrayview-0.42.1}/LICENSE +0 -0
  18. {arrayview-0.41.1 → arrayview-0.42.1}/README.md +0 -0
  19. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/ARCHITECTURE.md +0 -0
  20. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/__init__.py +0 -0
  21. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/__main__.py +0 -0
  22. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_analysis.py +0 -0
  23. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_app.py +0 -0
  24. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_config.py +0 -0
  25. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_dicom.py +0 -0
  26. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_diff.py +0 -0
  27. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_icon.png +0 -0
  28. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_imaging.py +0 -0
  29. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_instance_registry.py +0 -0
  30. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_io.py +0 -0
  31. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_launch_plan.py +0 -0
  32. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_lifecycle.py +0 -0
  33. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_overlays.py +0 -0
  34. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_platform.py +0 -0
  35. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_render.py +0 -0
  36. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_analysis.py +0 -0
  37. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_drop.py +0 -0
  38. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_export.py +0 -0
  39. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_loading.py +0 -0
  40. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_persistence.py +0 -0
  41. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_preferences.py +0 -0
  42. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_preload.py +0 -0
  43. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_rendering.py +0 -0
  44. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_segmentation.py +0 -0
  45. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_state.py +0 -0
  46. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_vectorfield.py +0 -0
  47. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_routes_websocket.py +0 -0
  48. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_segmentation.py +0 -0
  49. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_session.py +0 -0
  50. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_session_spec.py +0 -0
  51. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_shell.html +0 -0
  52. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_source_safety.py +0 -0
  53. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_synthetic_mri.py +0 -0
  54. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_torch.py +0 -0
  55. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_tutorial.py +0 -0
  56. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_vectorfield.py +0 -0
  57. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_vscode.py +0 -0
  58. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_vscode_browser.py +0 -0
  59. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_vscode_signal.py +0 -0
  60. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/_watch.py +0 -0
  61. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/gsap.min.js +0 -0
  62. {arrayview-0.41.1 → arrayview-0.42.1}/src/arrayview/todo.md +0 -0
  63. {arrayview-0.41.1 → arrayview-0.42.1}/tests/capture_patient_loading.py +0 -0
  64. {arrayview-0.41.1 → arrayview-0.42.1}/tests/capture_v_animation.py +0 -0
  65. {arrayview-0.41.1 → arrayview-0.42.1}/tests/conftest.py +0 -0
  66. {arrayview-0.41.1 → arrayview-0.42.1}/tests/lifecycle_matrix.py +0 -0
  67. {arrayview-0.41.1 → arrayview-0.42.1}/tests/make_vectorfield_test_arrays.py +0 -0
  68. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_1d_arrays.py +0 -0
  69. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_api.py +0 -0
  70. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_backend_shared.py +0 -0
  71. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_browser.py +0 -0
  72. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_cli.py +0 -0
  73. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_colorbar_hover_highlight.py +0 -0
  74. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_config.py +0 -0
  75. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_dicom.py +0 -0
  76. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_display_opener_results.py +0 -0
  77. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_doctor_commands.py +0 -0
  78. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_drop_import.py +0 -0
  79. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_instance_registry.py +0 -0
  80. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_invocation_contract_matrix.py +0 -0
  81. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_io.py +0 -0
  82. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_large_arrays.py +0 -0
  83. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_launch_blackbox.py +0 -0
  84. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_launch_execution_ownership.py +0 -0
  85. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_launch_plan.py +0 -0
  86. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_loading_server.py +0 -0
  87. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_mode_consistency.py +0 -0
  88. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_mode_matrix.py +0 -0
  89. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_mode_roundtrip.py +0 -0
  90. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_nifti_meta.py +0 -0
  91. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_nifti_series.py +0 -0
  92. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_preferences_ui.py +0 -0
  93. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_public_invocation_regressions.py +0 -0
  94. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_render_failure_reporting.py +0 -0
  95. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_render_progress.py +0 -0
  96. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_rgb_pixel_art.py +0 -0
  97. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_safe_server_stop.py +0 -0
  98. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_server_identity_status.py +0 -0
  99. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_session_spec.py +0 -0
  100. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_source_safety.py +0 -0
  101. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_spawn_ownership.py +0 -0
  102. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_torch.py +0 -0
  103. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_tutorial.py +0 -0
  104. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_tutorial_browser.py +0 -0
  105. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_value_formatting.py +0 -0
  106. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_view_handle_lifecycle.py +0 -0
  107. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_view_launch_execution.py +0 -0
  108. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_vscode_ack_protocol.py +0 -0
  109. {arrayview-0.41.1 → arrayview-0.42.1}/tests/test_window_level_preview.py +0 -0
  110. {arrayview-0.41.1 → arrayview-0.42.1}/tests/ui_audit.py +0 -0
  111. {arrayview-0.41.1 → arrayview-0.42.1}/tests/v_anim_frames/.gitkeep +0 -0
  112. {arrayview-0.41.1 → arrayview-0.42.1}/tests/visual_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arrayview
3
- Version: 0.41.1
3
+ Version: 0.42.1
4
4
  Summary: Fast multi-dimensional array viewer
5
5
  Project-URL: Home, https://github.com/oscarvanderheide/arrayview
6
6
  Project-URL: Source, https://github.com/oscarvanderheide/arrayview
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "arrayview"
7
- version = "0.41.1"
7
+ version = "0.42.1"
8
8
  description = "Fast multi-dimensional array viewer"
9
9
  readme = { file = "README.md", content-type = "text/markdown" }
10
10
  requires-python = ">=3.12"
@@ -76,7 +76,11 @@ _DIAGNOSES: list[tuple[str, str, str, str]] = [
76
76
  _RETRY),
77
77
 
78
78
  # ── Timing — slow, not broken ─────────────────────────────────────────
79
- (r"did not render a frame|did not become ready|did not start the viewer",
79
+ (r"did not start the viewer script", ACTION,
80
+ "VS Code opened an integrated-browser tab, but the tab did not navigate "
81
+ "to the ArrayView page.",
82
+ _RETRY + " If it repeats, reload the VS Code window."),
83
+ (r"did not render a frame|did not become ready",
80
84
  ACTION,
81
85
  "The viewer opened but showed no frame before the deadline — usually a "
82
86
  "slow first load.",
@@ -1,8 +1,10 @@
1
1
  """Best-effort, opt-in launch event tracing.
2
2
 
3
3
  The trace is deliberately dependency-free and inert unless
4
- ``ARRAYVIEW_LAUNCH_TRACE`` names an absolute JSONL file. Launch behavior must
5
- never depend on a trace write succeeding.
4
+ ``ARRAYVIEW_LAUNCH_TRACE`` names an absolute JSONL file. Integrated-browser
5
+ route tracing can also be enabled explicitly with
6
+ ``~/.arrayview/enable-launch-trace``. Launch behavior must never depend on a
7
+ trace write succeeding.
6
8
  """
7
9
 
8
10
  from __future__ import annotations
@@ -20,6 +22,8 @@ TRACE_PATH_ENV = "ARRAYVIEW_LAUNCH_TRACE"
20
22
  TRACE_ID_ENV = "ARRAYVIEW_LAUNCH_ID"
21
23
  TRACE_ROLE_ENV = "ARRAYVIEW_LAUNCH_ROLE"
22
24
  TRACE_SCHEMA = 1
25
+ TRACE_MARKER_NAME = "enable-launch-trace"
26
+ TRACE_DEFAULT_NAME = "launch-trace.jsonl"
23
27
  _MAX_EVENT_BYTES = 4096
24
28
 
25
29
  _lock = threading.Lock()
@@ -70,6 +74,56 @@ def trace_tag(value: object) -> str:
70
74
  return hashlib.sha256(str(value).encode("utf-8", "replace")).hexdigest()[:12]
71
75
 
72
76
 
77
+ def configure_route_launch_trace() -> bool:
78
+ """Enable route tracing from an explicit environment or marker opt-in.
79
+
80
+ Route handlers call this directly so an already-running server can notice
81
+ the marker even when its process environment never contained the trace
82
+ variable. An explicit environment value always wins, including an invalid
83
+ relative value, which keeps the normal trace contract unambiguous.
84
+ """
85
+ try:
86
+ explicit = os.environ.get(TRACE_PATH_ENV)
87
+ if explicit is not None:
88
+ if not explicit or not Path(explicit).is_absolute():
89
+ return False
90
+ if _path != explicit:
91
+ configure_launch_trace()
92
+ return _path == explicit
93
+
94
+ trace_dir = Path.home() / ".arrayview"
95
+ if not (trace_dir / TRACE_MARKER_NAME).is_file():
96
+ return False
97
+ candidate = str(trace_dir / TRACE_DEFAULT_NAME)
98
+ if _path != candidate:
99
+ configure_launch_trace(path=candidate, role="server")
100
+ return _path == candidate
101
+ except Exception:
102
+ return False
103
+
104
+
105
+ def emit_route_launch_event(
106
+ event: str,
107
+ *,
108
+ navigation_key: str,
109
+ tab_key: str | None = None,
110
+ **attrs: object,
111
+ ) -> None:
112
+ """Emit a route event without exposing private route credentials."""
113
+ try:
114
+ if not configure_route_launch_trace():
115
+ return
116
+ safe_attrs = {
117
+ "navigation_key_tag": trace_tag(navigation_key),
118
+ **attrs,
119
+ }
120
+ if tab_key is not None:
121
+ safe_attrs["tab_key_tag"] = trace_tag(tab_key)
122
+ emit_launch_event(event, **safe_attrs)
123
+ except Exception:
124
+ return
125
+
126
+
73
127
  def emit_launch_event(event: str, **attrs: object) -> None:
74
128
  """Append one compact event, swallowing every tracing-related failure."""
75
129
  try:
@@ -113,10 +167,13 @@ def _ensure_environment_configuration() -> None:
113
167
 
114
168
  __all__ = [
115
169
  "TRACE_ID_ENV",
170
+ "TRACE_MARKER_NAME",
116
171
  "TRACE_PATH_ENV",
117
172
  "TRACE_ROLE_ENV",
118
173
  "configure_launch_trace",
174
+ "configure_route_launch_trace",
119
175
  "emit_launch_event",
176
+ "emit_route_launch_event",
120
177
  "trace_child_environment",
121
178
  "trace_tag",
122
179
  ]
@@ -5266,15 +5266,56 @@ def _handle_management_command(argv: list[str]) -> bool:
5266
5266
  ),
5267
5267
  }
5268
5268
  )
5269
+ from arrayview._platform import _in_vscode_terminal # noqa: PLC0415
5270
+ from arrayview._vscode_extension import ( # noqa: PLC0415
5271
+ _describe_window,
5272
+ _opener_version_report,
5273
+ )
5274
+
5275
+ versions = _opener_version_report()
5276
+ if versions["state"] in {"window_older", "window_unversioned"}:
5277
+ remediation.append(
5278
+ {
5279
+ "code": "opener_version_skew",
5280
+ "message": (
5281
+ "Reload this VS Code window to run opener "
5282
+ f"v{versions['bundled_opener']}."
5283
+ ),
5284
+ }
5285
+ )
5269
5286
  report = {
5270
5287
  "snapshot": snapshot_dict,
5271
5288
  "plan": plan.to_dict(),
5272
5289
  "instances": rows,
5273
5290
  "remediation": remediation,
5291
+ "versions": versions,
5274
5292
  }
5275
5293
  if args.json:
5276
5294
  print(json.dumps(report, indent=2, sort_keys=True))
5277
5295
  else:
5296
+ print(
5297
+ f"arrayview {versions['package_version']} "
5298
+ f"({versions['package_path']})"
5299
+ )
5300
+ print(f"Opener bundled here: v{versions['bundled_opener']}")
5301
+ if versions["this_window"] is not None:
5302
+ suffix = {
5303
+ "window_older": " ← older than this arrayview; reload this window",
5304
+ "window_newer": " ← newer than this arrayview; kept and used",
5305
+ "window_unversioned": " ← pre-0.15 build; reload this window",
5306
+ }.get(versions["state"], "")
5307
+ print(
5308
+ "Opener live in this window: "
5309
+ f"{_describe_window(versions['this_window'])}{suffix}"
5310
+ )
5311
+ elif _in_vscode_terminal():
5312
+ print(
5313
+ "Opener live in this window: none — no live VS Code window "
5314
+ "claims this terminal"
5315
+ )
5316
+ for index, other in enumerate(versions["other_windows"]):
5317
+ label = "Opener live in other windows:" if index == 0 else " " * 29
5318
+ print(f"{label} {_describe_window(other)}")
5278
5319
  print(f"Environment: {plan.environment.value}")
5279
5320
  print(
5280
5321
  f"Server: {plan.server_owner.value} on port {plan.effective_port} "
@@ -60,12 +60,11 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
60
60
  raise HTTPException(status_code=400, detail="Invalid viewer phase")
61
61
  if phase != "launch-prepared" and not viewer_instance_id:
62
62
  raise HTTPException(status_code=400, detail="Incomplete viewer phase identity")
63
- # `launch-prepared` is control-plane bookkeeping. The viewer is meant
64
- # to open while a large array is still loading, so waiting for the data
65
- # Session here deadlocks that design against the opener's short journal
66
- # timeout. Pending registration is sufficient; later viewer phases
67
- # still wait for the real Session.
68
- if phase == "launch-prepared":
63
+ # Preparation and script startup are control-plane events. The viewer
64
+ # is meant to open while a large array is still loading, so hiding
65
+ # either event behind Session readiness creates a false pre-script
66
+ # timeout. Data-dependent phases still wait for the real Session.
67
+ if phase in {"launch-prepared", "script-loaded"}:
69
68
  if sid not in SESSIONS and sid not in _session_mod.PENDING_SESSIONS:
70
69
  raise HTTPException(status_code=404, detail="Session not found")
71
70
  else:
@@ -141,6 +140,20 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
141
140
  previous_navigation_key,
142
141
  None,
143
142
  )
143
+ try:
144
+ from arrayview._launch_trace import emit_route_launch_event
145
+
146
+ emit_route_launch_event(
147
+ "page.route_retired",
148
+ navigation_key=previous_navigation_key,
149
+ tab_key=previous_journal.get("tab_key"),
150
+ request_id=request_id,
151
+ navigation_attempt=previous_journal.get(
152
+ "navigation_attempt"
153
+ ),
154
+ )
155
+ except Exception:
156
+ pass
144
157
  journal = journals[request_id] = {
145
158
  "token": token,
146
159
  "window_id": window_id,
@@ -160,6 +173,18 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
160
173
  sid,
161
174
  request_id,
162
175
  )
176
+ try:
177
+ from arrayview._launch_trace import emit_route_launch_event
178
+
179
+ emit_route_launch_event(
180
+ "page.route_prepared",
181
+ navigation_key=navigation_key,
182
+ tab_key=tab_key,
183
+ request_id=request_id,
184
+ navigation_attempt=navigation_attempt,
185
+ )
186
+ except Exception:
187
+ pass
163
188
  return {
164
189
  "sid": sid,
165
190
  "request_id": request_id,
@@ -208,8 +233,7 @@ def register_query_routes(app, *, get_session_or_404, pil_image, pil_imageops) -
208
233
  @app.get("/viewer-phase/{sid}/{request_id}")
209
234
  async def get_viewer_phases(sid: str, request_id: str, token: str):
210
235
  """Return phases correlated to one browser launch transaction."""
211
- session = SESSIONS.get(sid)
212
- if session is None:
236
+ if sid not in SESSIONS and sid not in _session_mod.PENDING_SESSIONS:
213
237
  raise HTTPException(status_code=404, detail="Session not found")
214
238
  journals = _session_mod.VIEWER_PHASE_JOURNALS.get(sid, {})
215
239
  journal = journals.get(request_id, {})
@@ -422,6 +422,16 @@ def get_short_viewer_ui(
422
422
  navigation_key: str,
423
423
  ):
424
424
  """Resolve a private integrated-browser launch without exposing its query."""
425
+ try:
426
+ from arrayview._launch_trace import emit_route_launch_event
427
+
428
+ emit_route_launch_event(
429
+ "page.route_entered",
430
+ navigation_key=navigation_key,
431
+ tab_key=tab_key,
432
+ )
433
+ except Exception:
434
+ pass
425
435
  owner = _session_mod.VIEWER_LAUNCH_ROUTES.get(navigation_key)
426
436
  if owner is None:
427
437
  raise HTTPException(status_code=404, detail="Viewer launch route expired")
@@ -437,6 +447,18 @@ def get_short_viewer_ui(
437
447
  or journal.get("navigation_key") != navigation_key
438
448
  ):
439
449
  raise HTTPException(status_code=409, detail="Viewer launch route changed")
450
+ try:
451
+ from arrayview._launch_trace import emit_route_launch_event
452
+
453
+ emit_route_launch_event(
454
+ "page.route_resolved",
455
+ navigation_key=navigation_key,
456
+ tab_key=tab_key,
457
+ request_id=request_id,
458
+ navigation_attempt=journal["navigation_attempt"],
459
+ )
460
+ except Exception:
461
+ pass
440
462
  bootstrap_query = journal["viewer_query"] + "&" + urlencode(
441
463
  {
442
464
  "_av_integrated_browser": "1",