zig-mobile-runner 0.1.0
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.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const selector = @import("selector.zig");
|
|
3
|
+
const types = @import("types.zig");
|
|
4
|
+
|
|
5
|
+
pub const RedactionRules = struct {
|
|
6
|
+
denylist_text: []const []const u8 = &.{},
|
|
7
|
+
allowlist_text: []const []const u8 = &.{},
|
|
8
|
+
denylist_resource_ids: []const []const u8 = &.{},
|
|
9
|
+
allowlist_resource_ids: []const []const u8 = &.{},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
pub fn writeSnapshotJson(writer: anytype, snapshot: types.ObservationSnapshot) !void {
|
|
13
|
+
try writer.writeAll("{");
|
|
14
|
+
try jsonField(writer, "id", snapshot.id, true);
|
|
15
|
+
try writer.print(",\"timestampMs\":{d}", .{snapshot.timestamp_ms});
|
|
16
|
+
try writer.print(
|
|
17
|
+
",\"viewport\":{{\"width\":{d},\"height\":{d}}}",
|
|
18
|
+
.{ snapshot.viewport.width, snapshot.viewport.height },
|
|
19
|
+
);
|
|
20
|
+
try writer.writeAll(",\"displayDensityDpi\":");
|
|
21
|
+
if (snapshot.display_density_dpi) |density| {
|
|
22
|
+
try writer.print("{d}", .{density});
|
|
23
|
+
} else {
|
|
24
|
+
try writer.writeAll("null");
|
|
25
|
+
}
|
|
26
|
+
try jsonNullableField(writer, "activePackage", snapshot.active_package);
|
|
27
|
+
try jsonNullableField(writer, "activeActivity", snapshot.active_activity);
|
|
28
|
+
try jsonNullableField(writer, "screenshotArtifact", snapshot.screenshot_artifact);
|
|
29
|
+
try jsonNullableField(writer, "treeArtifact", snapshot.tree_artifact);
|
|
30
|
+
try jsonNullableField(writer, "focusedNodeId", snapshot.focused_node_id);
|
|
31
|
+
try jsonNullableField(writer, "logDelta", snapshot.log_delta);
|
|
32
|
+
try writer.writeAll(",\"nodes\":[");
|
|
33
|
+
for (snapshot.nodes, 0..) |node, index| {
|
|
34
|
+
if (index > 0) try writer.writeAll(",");
|
|
35
|
+
try writeNodeJson(writer, node);
|
|
36
|
+
}
|
|
37
|
+
try writer.writeAll("]}");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn writeNodeJson(writer: anytype, node: types.UiNode) !void {
|
|
41
|
+
try writer.writeAll("{");
|
|
42
|
+
try jsonField(writer, "stableId", node.stable_id, true);
|
|
43
|
+
try jsonField(writer, "className", node.class_name, false);
|
|
44
|
+
try jsonNullableField(writer, "resourceId", node.resource_id);
|
|
45
|
+
try jsonNullableField(writer, "text", node.text);
|
|
46
|
+
try jsonNullableField(writer, "contentDesc", node.content_desc);
|
|
47
|
+
try writer.print(
|
|
48
|
+
",\"bounds\":{{\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d}}}",
|
|
49
|
+
.{ node.bounds.x, node.bounds.y, node.bounds.width, node.bounds.height },
|
|
50
|
+
);
|
|
51
|
+
try writer.print(
|
|
52
|
+
",\"enabled\":{},\"visible\":{},\"selected\":{}",
|
|
53
|
+
.{ node.enabled, node.visible, node.selected },
|
|
54
|
+
);
|
|
55
|
+
try writer.writeAll("}");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn writeSnapshotJsonRedacted(writer: anytype, snapshot: types.ObservationSnapshot, redaction: RedactionRules) !void {
|
|
59
|
+
try writer.writeAll("{");
|
|
60
|
+
try jsonField(writer, "id", snapshot.id, true);
|
|
61
|
+
try writer.print(",\"timestampMs\":{d}", .{snapshot.timestamp_ms});
|
|
62
|
+
try writer.print(
|
|
63
|
+
",\"viewport\":{{\"width\":{d},\"height\":{d}}}",
|
|
64
|
+
.{ snapshot.viewport.width, snapshot.viewport.height },
|
|
65
|
+
);
|
|
66
|
+
try writer.writeAll(",\"displayDensityDpi\":");
|
|
67
|
+
if (snapshot.display_density_dpi) |density| {
|
|
68
|
+
try writer.print("{d}", .{density});
|
|
69
|
+
} else {
|
|
70
|
+
try writer.writeAll("null");
|
|
71
|
+
}
|
|
72
|
+
try jsonNullableField(writer, "activePackage", snapshot.active_package);
|
|
73
|
+
try jsonNullableField(writer, "activeActivity", snapshot.active_activity);
|
|
74
|
+
try jsonNullableField(writer, "screenshotArtifact", snapshot.screenshot_artifact);
|
|
75
|
+
try jsonNullableField(writer, "treeArtifact", snapshot.tree_artifact);
|
|
76
|
+
try jsonNullableField(writer, "focusedNodeId", snapshot.focused_node_id);
|
|
77
|
+
try jsonNullableFieldRedacted(writer, "logDelta", snapshot.log_delta, redaction);
|
|
78
|
+
try writer.writeAll(",\"nodes\":[");
|
|
79
|
+
for (snapshot.nodes, 0..) |node, index| {
|
|
80
|
+
if (index > 0) try writer.writeAll(",");
|
|
81
|
+
try writeNodeJsonRedacted(writer, node, redaction);
|
|
82
|
+
}
|
|
83
|
+
try writer.writeAll("]}");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn writeNodeJsonRedacted(writer: anytype, node: types.UiNode, redaction: RedactionRules) !void {
|
|
87
|
+
try writer.writeAll("{");
|
|
88
|
+
try jsonField(writer, "stableId", node.stable_id, true);
|
|
89
|
+
try jsonField(writer, "className", node.class_name, false);
|
|
90
|
+
const sensitive_node = if (node.resource_id) |id| isSensitiveResourceId(id, redaction) else false;
|
|
91
|
+
try jsonNullableResourceIdRedacted(writer, "resourceId", node.resource_id, redaction);
|
|
92
|
+
try jsonNullableFieldRedactedWithContext(writer, "text", node.text, sensitive_node, redaction);
|
|
93
|
+
try jsonNullableFieldRedactedWithContext(writer, "contentDesc", node.content_desc, sensitive_node, redaction);
|
|
94
|
+
try writer.print(
|
|
95
|
+
",\"bounds\":{{\"x\":{d},\"y\":{d},\"width\":{d},\"height\":{d}}}",
|
|
96
|
+
.{ node.bounds.x, node.bounds.y, node.bounds.width, node.bounds.height },
|
|
97
|
+
);
|
|
98
|
+
try writer.print(
|
|
99
|
+
",\"enabled\":{},\"visible\":{},\"selected\":{}",
|
|
100
|
+
.{ node.enabled, node.visible, node.selected },
|
|
101
|
+
);
|
|
102
|
+
try writer.writeAll("}");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn writeSelectorJson(writer: anytype, wanted: selector.Selector) !void {
|
|
106
|
+
try writer.writeAll("{");
|
|
107
|
+
var first = true;
|
|
108
|
+
if (wanted.id) |value| {
|
|
109
|
+
try jsonField(writer, "id", value, first);
|
|
110
|
+
first = false;
|
|
111
|
+
}
|
|
112
|
+
if (wanted.text) |value| {
|
|
113
|
+
try jsonField(writer, "text", value, first);
|
|
114
|
+
first = false;
|
|
115
|
+
}
|
|
116
|
+
if (wanted.text_contains) |value| {
|
|
117
|
+
try jsonField(writer, "textContains", value, first);
|
|
118
|
+
first = false;
|
|
119
|
+
}
|
|
120
|
+
if (wanted.content_desc) |value| {
|
|
121
|
+
try jsonField(writer, "contentDesc", value, first);
|
|
122
|
+
first = false;
|
|
123
|
+
}
|
|
124
|
+
if (wanted.content_desc_contains) |value| {
|
|
125
|
+
try jsonField(writer, "contentDescContains", value, first);
|
|
126
|
+
first = false;
|
|
127
|
+
}
|
|
128
|
+
if (wanted.class_name) |value| {
|
|
129
|
+
try jsonField(writer, "className", value, first);
|
|
130
|
+
}
|
|
131
|
+
try writer.writeAll("}");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn jsonField(writer: anytype, key: []const u8, value: []const u8, first: bool) !void {
|
|
135
|
+
if (!first) try writer.writeAll(",");
|
|
136
|
+
try writer.print("\"{s}\":", .{key});
|
|
137
|
+
try writeJsonString(writer, value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fn jsonNullableField(writer: anytype, key: []const u8, value: ?[]const u8) !void {
|
|
141
|
+
try writer.print(",\"{s}\":", .{key});
|
|
142
|
+
if (value) |actual| {
|
|
143
|
+
try writeJsonString(writer, actual);
|
|
144
|
+
} else {
|
|
145
|
+
try writer.writeAll("null");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn jsonNullableFieldRedacted(writer: anytype, key: []const u8, value: ?[]const u8, redaction: RedactionRules) !void {
|
|
150
|
+
try jsonNullableFieldRedactedWithContext(writer, key, value, false, redaction);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn jsonNullableFieldRedactedWithContext(writer: anytype, key: []const u8, value: ?[]const u8, force_secret: bool, redaction: RedactionRules) !void {
|
|
154
|
+
try writer.print(",\"{s}\":", .{key});
|
|
155
|
+
if (value) |actual| {
|
|
156
|
+
try writeRedactedJsonStringForKeyWithRules(writer, key, actual, force_secret, redaction);
|
|
157
|
+
} else {
|
|
158
|
+
try writer.writeAll("null");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn jsonNullableResourceIdRedacted(writer: anytype, key: []const u8, value: ?[]const u8, redaction: RedactionRules) !void {
|
|
163
|
+
try writer.print(",\"{s}\":", .{key});
|
|
164
|
+
if (value) |actual| {
|
|
165
|
+
if (resourceIdDenied(actual, redaction) and !resourceIdAllowed(actual, redaction)) {
|
|
166
|
+
try writeJsonString(writer, "[REDACTED:resourceId]");
|
|
167
|
+
} else {
|
|
168
|
+
try writeJsonString(writer, actual);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
try writer.writeAll("null");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn writeRedactedJsonPayload(allocator: std.mem.Allocator, writer: anytype, payload: []const u8, redaction: RedactionRules) !void {
|
|
176
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, payload, .{}) catch {
|
|
177
|
+
try writeRedactedJsonStringWithRules(writer, payload, redaction);
|
|
178
|
+
return;
|
|
179
|
+
};
|
|
180
|
+
defer parsed.deinit();
|
|
181
|
+
try writeJsonValueRedacted(writer, parsed.value, null, redaction);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn writeJsonValueRedacted(writer: anytype, value: std.json.Value, key_context: ?[]const u8, redaction: RedactionRules) !void {
|
|
185
|
+
switch (value) {
|
|
186
|
+
.null => try writer.writeAll("null"),
|
|
187
|
+
.bool => |actual| try writer.writeAll(if (actual) "true" else "false"),
|
|
188
|
+
.integer => |actual| try writer.print("{d}", .{actual}),
|
|
189
|
+
.float => |actual| try writer.print("{d}", .{actual}),
|
|
190
|
+
.number_string => |actual| try writer.writeAll(actual),
|
|
191
|
+
.string => |actual| {
|
|
192
|
+
if (key_context) |key| {
|
|
193
|
+
try writeRedactedJsonStringForKeyWithRules(writer, key, actual, false, redaction);
|
|
194
|
+
} else {
|
|
195
|
+
try writeRedactedJsonStringWithRules(writer, actual, redaction);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
.array => |array| {
|
|
199
|
+
try writer.writeAll("[");
|
|
200
|
+
for (array.items, 0..) |item, index| {
|
|
201
|
+
if (index > 0) try writer.writeAll(",");
|
|
202
|
+
try writeJsonValueRedacted(writer, item, key_context, redaction);
|
|
203
|
+
}
|
|
204
|
+
try writer.writeAll("]");
|
|
205
|
+
},
|
|
206
|
+
.object => |object| {
|
|
207
|
+
try writer.writeAll("{");
|
|
208
|
+
var first = true;
|
|
209
|
+
var iterator = object.iterator();
|
|
210
|
+
while (iterator.next()) |entry| {
|
|
211
|
+
if (!first) try writer.writeAll(",");
|
|
212
|
+
first = false;
|
|
213
|
+
try writeJsonString(writer, entry.key_ptr.*);
|
|
214
|
+
try writer.writeAll(":");
|
|
215
|
+
try writeJsonValueRedacted(writer, entry.value_ptr.*, entry.key_ptr.*, redaction);
|
|
216
|
+
}
|
|
217
|
+
try writer.writeAll("}");
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
pub fn writeRedactedJsonString(writer: anytype, value: []const u8) !void {
|
|
223
|
+
try writeRedactedJsonStringForKeyWithRules(writer, "", value, false, .{});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
pub fn writeRedactedJsonStringForKey(writer: anytype, key: []const u8, value: []const u8, force_secret: bool) !void {
|
|
227
|
+
try writeRedactedJsonStringForKeyWithRules(writer, key, value, force_secret, .{});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
fn writeRedactedJsonStringWithRules(writer: anytype, value: []const u8, redaction: RedactionRules) !void {
|
|
231
|
+
try writeRedactedJsonStringForKeyWithRules(writer, "", value, false, redaction);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn writeRedactedJsonStringForKeyWithRules(writer: anytype, key: []const u8, value: []const u8, force_secret: bool, redaction: RedactionRules) !void {
|
|
235
|
+
if (force_secret or isSensitiveLabel(key)) {
|
|
236
|
+
try writeJsonString(writer, "[REDACTED:secret]");
|
|
237
|
+
} else if (textDenied(value, redaction) and !textAllowed(value, redaction)) {
|
|
238
|
+
try writeJsonString(writer, "[REDACTED:custom]");
|
|
239
|
+
} else if (looksLikeToken(value)) {
|
|
240
|
+
try writeJsonString(writer, "[REDACTED:token]");
|
|
241
|
+
} else if (looksLikeEmail(value)) {
|
|
242
|
+
try writeJsonString(writer, "[REDACTED:email]");
|
|
243
|
+
} else {
|
|
244
|
+
try writeJsonString(writer, value);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
pub fn writeJsonString(writer: anytype, value: []const u8) !void {
|
|
249
|
+
try writer.writeAll("\"");
|
|
250
|
+
for (value) |ch| {
|
|
251
|
+
switch (ch) {
|
|
252
|
+
'"' => try writer.writeAll("\\\""),
|
|
253
|
+
'\\' => try writer.writeAll("\\\\"),
|
|
254
|
+
'\n' => try writer.writeAll("\\n"),
|
|
255
|
+
'\r' => try writer.writeAll("\\r"),
|
|
256
|
+
'\t' => try writer.writeAll("\\t"),
|
|
257
|
+
0...7, 11, 12, 14...31 => try writer.print("\\u{x:0>4}", .{ch}),
|
|
258
|
+
else => try writer.writeAll(&.{ch}),
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
try writer.writeAll("\"");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn isSensitiveLabel(value: []const u8) bool {
|
|
265
|
+
const needles = [_][]const u8{
|
|
266
|
+
"password",
|
|
267
|
+
"token",
|
|
268
|
+
"secret",
|
|
269
|
+
"authorization",
|
|
270
|
+
"auth",
|
|
271
|
+
"cookie",
|
|
272
|
+
"apikey",
|
|
273
|
+
"api_key",
|
|
274
|
+
"bearer",
|
|
275
|
+
};
|
|
276
|
+
for (needles) |needle| {
|
|
277
|
+
if (indexOfIgnoreCase(value, needle) != null) return true;
|
|
278
|
+
}
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fn isSensitiveResourceId(value: []const u8, redaction: RedactionRules) bool {
|
|
283
|
+
if (resourceIdAllowed(value, redaction)) return false;
|
|
284
|
+
return isSensitiveLabel(value) or resourceIdDenied(value, redaction);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn resourceIdDenied(value: []const u8, redaction: RedactionRules) bool {
|
|
288
|
+
return matchesAnyIgnoreCase(value, redaction.denylist_resource_ids);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
fn resourceIdAllowed(value: []const u8, redaction: RedactionRules) bool {
|
|
292
|
+
return matchesAnyIgnoreCase(value, redaction.allowlist_resource_ids);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
fn textDenied(value: []const u8, redaction: RedactionRules) bool {
|
|
296
|
+
return matchesAnyIgnoreCase(value, redaction.denylist_text);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
fn textAllowed(value: []const u8, redaction: RedactionRules) bool {
|
|
300
|
+
return matchesAnyIgnoreCase(value, redaction.allowlist_text);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn matchesAnyIgnoreCase(value: []const u8, needles: []const []const u8) bool {
|
|
304
|
+
for (needles) |needle| {
|
|
305
|
+
if (indexOfIgnoreCase(value, needle) != null) return true;
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn looksLikeEmail(value: []const u8) bool {
|
|
311
|
+
if (value.len < 5 or value.len > 254) return false;
|
|
312
|
+
if (std.mem.indexOfAny(u8, value, " \t\r\n<>") != null) return false;
|
|
313
|
+
const at = std.mem.indexOfScalar(u8, value, '@') orelse return false;
|
|
314
|
+
if (at == 0 or at + 3 >= value.len) return false;
|
|
315
|
+
return std.mem.indexOfScalar(u8, value[at + 1 ..], '.') != null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn looksLikeToken(value: []const u8) bool {
|
|
319
|
+
if (indexOfIgnoreCase(value, "bearer ") != null) return true;
|
|
320
|
+
if (value.len < 40) return false;
|
|
321
|
+
const first_dot = std.mem.indexOfScalar(u8, value, '.') orelse return false;
|
|
322
|
+
const second_dot = std.mem.indexOfScalarPos(u8, value, first_dot + 1, '.') orelse return false;
|
|
323
|
+
return second_dot + 1 < value.len;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
fn indexOfIgnoreCase(haystack: []const u8, needle: []const u8) ?usize {
|
|
327
|
+
if (needle.len == 0 or needle.len > haystack.len) return null;
|
|
328
|
+
var index: usize = 0;
|
|
329
|
+
while (index + needle.len <= haystack.len) : (index += 1) {
|
|
330
|
+
var matched = true;
|
|
331
|
+
for (needle, 0..) |needle_ch, offset| {
|
|
332
|
+
if (std.ascii.toLower(haystack[index + offset]) != std.ascii.toLower(needle_ch)) {
|
|
333
|
+
matched = false;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (matched) return index;
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const trace = @import("trace.zig");
|
|
3
|
+
const trace_summary_diagnostic = @import("trace_summary_diagnostic.zig");
|
|
4
|
+
|
|
5
|
+
pub const DiagnosticEvent = trace_summary_diagnostic.DiagnosticEvent;
|
|
6
|
+
|
|
7
|
+
pub const Summary = struct {
|
|
8
|
+
scenario_name: []u8,
|
|
9
|
+
status: []u8,
|
|
10
|
+
app_id: ?[]u8 = null,
|
|
11
|
+
events_path: []u8,
|
|
12
|
+
artifacts_dir: []u8,
|
|
13
|
+
duration_ms: ?i64 = null,
|
|
14
|
+
event_count: ?i64 = null,
|
|
15
|
+
snapshot_count: ?i64 = null,
|
|
16
|
+
partial_failure_count: ?i64 = null,
|
|
17
|
+
failed_step_index: ?i64 = null,
|
|
18
|
+
error_name: ?[]u8 = null,
|
|
19
|
+
report_path: ?[]u8 = null,
|
|
20
|
+
diagnostic: DiagnosticEvent = .{},
|
|
21
|
+
partial_failure: ?DiagnosticEvent = null,
|
|
22
|
+
last_kind: ?[]u8 = null,
|
|
23
|
+
|
|
24
|
+
pub fn deinit(self: *Summary, allocator: std.mem.Allocator) void {
|
|
25
|
+
allocator.free(self.scenario_name);
|
|
26
|
+
allocator.free(self.status);
|
|
27
|
+
if (self.app_id) |value| allocator.free(value);
|
|
28
|
+
allocator.free(self.events_path);
|
|
29
|
+
allocator.free(self.artifacts_dir);
|
|
30
|
+
if (self.error_name) |value| allocator.free(value);
|
|
31
|
+
if (self.report_path) |value| allocator.free(value);
|
|
32
|
+
self.diagnostic.deinit(allocator);
|
|
33
|
+
if (self.partial_failure) |*value| value.deinit(allocator);
|
|
34
|
+
if (self.last_kind) |value| allocator.free(value);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const TerminalEvent = struct {
|
|
39
|
+
status: ?[]u8 = null,
|
|
40
|
+
error_name: ?[]u8 = null,
|
|
41
|
+
failed_step_index: ?i64 = null,
|
|
42
|
+
|
|
43
|
+
fn deinit(self: *TerminalEvent, allocator: std.mem.Allocator) void {
|
|
44
|
+
if (self.status) |value| allocator.free(value);
|
|
45
|
+
if (self.error_name) |value| allocator.free(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn setStatus(self: *TerminalEvent, allocator: std.mem.Allocator, value: ?[]const u8) !void {
|
|
49
|
+
if (self.status) |old| allocator.free(old);
|
|
50
|
+
self.status = if (value) |actual| try allocator.dupe(u8, actual) else null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn setErrorName(self: *TerminalEvent, allocator: std.mem.Allocator, value: ?[]const u8) !void {
|
|
54
|
+
if (self.error_name) |old| allocator.free(old);
|
|
55
|
+
self.error_name = if (value) |actual| try allocator.dupe(u8, actual) else null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
pub fn read(allocator: std.mem.Allocator, trace_dir: []const u8) !Summary {
|
|
60
|
+
const manifest_path = try std.fs.path.join(allocator, &.{ trace_dir, "trace.json" });
|
|
61
|
+
defer allocator.free(manifest_path);
|
|
62
|
+
const manifest_content = try std.fs.cwd().readFileAlloc(allocator, manifest_path, 1024 * 1024);
|
|
63
|
+
defer allocator.free(manifest_content);
|
|
64
|
+
|
|
65
|
+
const manifest = try std.json.parseFromSlice(std.json.Value, allocator, manifest_content, .{});
|
|
66
|
+
defer manifest.deinit();
|
|
67
|
+
if (manifest.value != .object) return error.InvalidTraceManifest;
|
|
68
|
+
const manifest_object = manifest.value.object;
|
|
69
|
+
|
|
70
|
+
const scenario_name = stringField(manifest_object, "scenarioName") orelse "";
|
|
71
|
+
const manifest_status = stringField(manifest_object, "status") orelse "";
|
|
72
|
+
const app_id = stringField(manifest_object, "appId");
|
|
73
|
+
const error_name = stringField(manifest_object, "error");
|
|
74
|
+
const events_path_value = stringField(manifest_object, "eventsPath") orelse "events.jsonl";
|
|
75
|
+
const artifacts_dir_value = stringField(manifest_object, "artifactsDir") orelse "artifacts";
|
|
76
|
+
const report_path = stringField(manifest_object, "reportPath");
|
|
77
|
+
const failed_step_index = intField(manifest_object, "failedStepIndex");
|
|
78
|
+
const duration_ms = intField(manifest_object, "durationMs");
|
|
79
|
+
const event_count = intField(manifest_object, "eventCount");
|
|
80
|
+
const snapshot_count = intField(manifest_object, "snapshotCount");
|
|
81
|
+
const partial_failure_count = intField(manifest_object, "partialFailureCount");
|
|
82
|
+
|
|
83
|
+
var terminal = TerminalEvent{};
|
|
84
|
+
defer terminal.deinit(allocator);
|
|
85
|
+
var diagnostic = DiagnosticEvent{};
|
|
86
|
+
defer diagnostic.deinit(allocator);
|
|
87
|
+
var partial_failure: ?DiagnosticEvent = null;
|
|
88
|
+
defer if (partial_failure) |*value| value.deinit(allocator);
|
|
89
|
+
var last_kind: ?[]u8 = null;
|
|
90
|
+
defer if (last_kind) |value| allocator.free(value);
|
|
91
|
+
|
|
92
|
+
const events_path = try std.fs.path.join(allocator, &.{ trace_dir, events_path_value });
|
|
93
|
+
defer allocator.free(events_path);
|
|
94
|
+
if (std.fs.cwd().readFileAlloc(allocator, events_path, 64 * 1024 * 1024)) |events_content| {
|
|
95
|
+
defer allocator.free(events_content);
|
|
96
|
+
try scanEvents(allocator, events_content, &terminal, &diagnostic, &partial_failure, &last_kind);
|
|
97
|
+
} else |err| switch (err) {
|
|
98
|
+
error.FileNotFound => {},
|
|
99
|
+
else => return err,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const terminal_status = terminal.status orelse manifest_status;
|
|
103
|
+
const effective_status = if (std.mem.eql(u8, manifest_status, "partial") and std.mem.eql(u8, terminal_status, "passed"))
|
|
104
|
+
manifest_status
|
|
105
|
+
else
|
|
106
|
+
terminal_status;
|
|
107
|
+
|
|
108
|
+
return .{
|
|
109
|
+
.scenario_name = try allocator.dupe(u8, scenario_name),
|
|
110
|
+
.status = try allocator.dupe(u8, effective_status),
|
|
111
|
+
.app_id = try dupeOptionalString(allocator, app_id),
|
|
112
|
+
.events_path = try allocator.dupe(u8, events_path_value),
|
|
113
|
+
.artifacts_dir = try allocator.dupe(u8, artifacts_dir_value),
|
|
114
|
+
.duration_ms = duration_ms,
|
|
115
|
+
.event_count = event_count,
|
|
116
|
+
.snapshot_count = snapshot_count,
|
|
117
|
+
.partial_failure_count = partial_failure_count,
|
|
118
|
+
.failed_step_index = terminal.failed_step_index orelse failed_step_index,
|
|
119
|
+
.error_name = try dupeOptionalString(allocator, terminal.error_name orelse error_name),
|
|
120
|
+
.report_path = try dupeOptionalString(allocator, report_path),
|
|
121
|
+
.diagnostic = blk: {
|
|
122
|
+
const value = diagnostic;
|
|
123
|
+
diagnostic = .{};
|
|
124
|
+
break :blk value;
|
|
125
|
+
},
|
|
126
|
+
.partial_failure = blk: {
|
|
127
|
+
const value = partial_failure;
|
|
128
|
+
partial_failure = null;
|
|
129
|
+
break :blk value;
|
|
130
|
+
},
|
|
131
|
+
.last_kind = blk: {
|
|
132
|
+
const value = last_kind;
|
|
133
|
+
last_kind = null;
|
|
134
|
+
break :blk value;
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn scanEvents(
|
|
140
|
+
allocator: std.mem.Allocator,
|
|
141
|
+
events_content: []const u8,
|
|
142
|
+
terminal: *TerminalEvent,
|
|
143
|
+
diagnostic: *DiagnosticEvent,
|
|
144
|
+
partial_failure: *?DiagnosticEvent,
|
|
145
|
+
last_kind: *?[]u8,
|
|
146
|
+
) !void {
|
|
147
|
+
var lines = std.mem.splitScalar(u8, events_content, '\n');
|
|
148
|
+
while (lines.next()) |raw_line| {
|
|
149
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
150
|
+
if (line.len == 0) continue;
|
|
151
|
+
|
|
152
|
+
const parsed = std.json.parseFromSlice(std.json.Value, allocator, line, .{}) catch continue;
|
|
153
|
+
defer parsed.deinit();
|
|
154
|
+
if (parsed.value != .object) continue;
|
|
155
|
+
const object = parsed.value.object;
|
|
156
|
+
const kind = stringField(object, "kind") orelse continue;
|
|
157
|
+
if (last_kind.*) |old| allocator.free(old);
|
|
158
|
+
last_kind.* = try allocator.dupe(u8, kind);
|
|
159
|
+
|
|
160
|
+
const payload_value = object.get("payload") orelse continue;
|
|
161
|
+
if (payload_value != .object) continue;
|
|
162
|
+
const payload = payload_value.object;
|
|
163
|
+
|
|
164
|
+
if (std.mem.eql(u8, kind, "scenario.end")) {
|
|
165
|
+
try terminal.setStatus(allocator, stringField(payload, "status"));
|
|
166
|
+
try terminal.setErrorName(allocator, stringField(payload, "error"));
|
|
167
|
+
terminal.failed_step_index = intField(payload, "failedStepIndex");
|
|
168
|
+
} else if (isDiagnosticKind(kind, payload)) {
|
|
169
|
+
diagnostic.deinit(allocator);
|
|
170
|
+
diagnostic.* = try DiagnosticEvent.fromPayload(allocator, kind, payload);
|
|
171
|
+
if (isPartialFailureEvent(kind, payload)) {
|
|
172
|
+
if (partial_failure.*) |*old| old.deinit(allocator);
|
|
173
|
+
partial_failure.* = try DiagnosticEvent.fromPayload(allocator, kind, payload);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
pub fn writeDiagnosticJson(writer: anytype, diagnostic: DiagnosticEvent) !void {
|
|
180
|
+
try trace_summary_diagnostic.writeJson(writer, diagnostic);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pub fn writePartialFailureJson(writer: anytype, partial: DiagnosticEvent) !void {
|
|
184
|
+
try trace_summary_diagnostic.writePartialJson(writer, partial);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn isDiagnosticKind(kind: []const u8, payload: std.json.ObjectMap) bool {
|
|
188
|
+
if (isPartialFailureEvent(kind, payload)) return true;
|
|
189
|
+
if (payload.get("snapshotId") == null) return false;
|
|
190
|
+
return std.mem.indexOf(u8, kind, "wait.") != null or
|
|
191
|
+
std.mem.indexOf(u8, kind, "notFound") != null or
|
|
192
|
+
std.mem.indexOf(u8, kind, "scrollUntilVisible") != null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn isPartialFailureEvent(kind: []const u8, payload: std.json.ObjectMap) bool {
|
|
196
|
+
if (!std.mem.eql(u8, kind, "observe.snapshot.semanticExtraction")) return false;
|
|
197
|
+
return std.mem.eql(u8, stringField(payload, "artifactStatus") orelse "", "captured") and
|
|
198
|
+
std.mem.eql(u8, stringField(payload, "semanticStatus") orelse "", "failed");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
fn dupeOptionalString(allocator: std.mem.Allocator, value: ?[]const u8) !?[]u8 {
|
|
202
|
+
if (value) |actual| return try allocator.dupe(u8, actual);
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
fn stringField(object: std.json.ObjectMap, key: []const u8) ?[]const u8 {
|
|
207
|
+
const value = object.get(key) orelse return null;
|
|
208
|
+
if (value != .string) return null;
|
|
209
|
+
return value.string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fn intField(object: std.json.ObjectMap, key: []const u8) ?i64 {
|
|
213
|
+
const value = object.get(key) orelse return null;
|
|
214
|
+
return switch (value) {
|
|
215
|
+
.integer => |actual| actual,
|
|
216
|
+
else => null,
|
|
217
|
+
};
|
|
218
|
+
}
|