Skip to content

Commit a864a48

Browse files
authored
Merge pull request #35 from blendto/codex/production-event-parameters
feat: allow production event parameter capture
2 parents 0a279a4 + 5fdbd14 commit a864a48

12 files changed

Lines changed: 163 additions & 25 deletions

File tree

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ should be verified in their own repositories.
2828

2929
## Current compatibility
3030

31-
- package version: `0.6.0`;
31+
- package version: `0.7.0`;
3232
- session JSON schema: `9`;
3333
- fingerprint schema: `6`;
3434
- minimum Dart SDK: `3.9.2`;

docs/integration/production-replay-acceptance.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ database receipt alone as proof that a replay is correct.
1212

1313
## Current acceptance status
1414

15-
The current SDK release candidate is **0.5.0**, which writes session schema
15+
The current SDK release candidate is **0.7.0**, which writes session schema
1616
**v9**. It preserves structural interaction replay while no longer emitting
1717
`controlValue`, `controlValueTransition`, or `semanticAnnotation` in event
1818
data. Treat the absence of those fields as the expected privacy boundary, not
@@ -164,7 +164,7 @@ flows share a session, list the event IDs or timestamps that delimit each flow.
164164

165165
Wait until the collector session has finalized and the replay is available in
166166
the production website. Filter to the recorded Blend build and SDK version
167-
under test (`0.5.0` for this release), then open every recorded session.
167+
under test (`0.7.0` for this release), then open every recorded session.
168168

169169
For each interaction, inspect the actual replay UI and verify:
170170

packages/tugboat/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## 0.7.0
2+
3+
### Added
4+
5+
- **Explicit production parameter opt-in**
6+
`TugboatParameterPolicy.allowAllInProduction` retains JSON-safe external-event
7+
parameter values in production capture profiles. `namesOnly` remains the
8+
default. `allowAll` remains an exploration-only escape hatch and still
9+
downgrades to names-only outside exploration. The existing JSON and size
10+
bounds still apply. This policy can retain feedback, search terms, URLs, IDs,
11+
and other user content. Hosts must confirm consent, privacy, access, and
12+
retention rules before they use it.
13+
114
## 0.6.0
215

316
### Changed

packages/tugboat/README.md

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ checkpoints around meaningful interactions, compact structural anchors, route
55
transitions, scrolling evidence, and optional viewport semantic maps. Capture
66
can be sent to the local exploration WebSocket, the HTTP collector, or both.
77

8-
The current package version is `0.6.0`. Session JSON writers emit schema
8+
The current package version is `0.7.0`. Session JSON writers emit schema
99
version `9`; compatibility readers should accept versions `6` through
1010
`9`. Structural fingerprints use fingerprint schema version `6`.
1111

@@ -23,7 +23,7 @@ The package requires Dart 3.9.2 or newer and Flutter 3.35.0 or newer.
2323

2424
```yaml
2525
dependencies:
26-
tugboat_dio: ^0.6.0
26+
tugboat_dio: ^0.7.0
2727
```
2828
2929
See `packages/tugboat_dio/README.md`.
@@ -58,12 +58,35 @@ failedCall.complete(
5858
```
5959

6060
Both emit on `stream: evidence` and never inherit exploration `actionId` or UI
61-
anchors. Parameter values are omitted unless an explicit policy allows them.
62-
`allowAll` is an exploration escape hatch; outside exploration profiles the SDK
63-
downgrades it to names-only at record time. Network routes must be absolute path
64-
templates. The SDK drops resolver output containing a scheme, query, fragment,
65-
percent-encoded data, a network-path prefix, backslash, or whitespace/control
66-
characters; host resolvers must still replace dynamic IDs with placeholders.
61+
anchors. `namesOnly` is the default parameter policy. It retains parameter keys
62+
but omits parameter values. `allowAll` is an exploration-only escape hatch;
63+
outside exploration profiles the SDK downgrades it to names-only at record time.
64+
65+
### Production parameter values
66+
67+
Use `allowAllInProduction` only when the host needs to retain all JSON-safe
68+
parameter values in a production capture profile:
69+
70+
```dart
71+
final productionEvents = TugboatReplay.eventHook(
72+
source: 'feedback',
73+
parameterPolicy: TugboatParameterPolicy.allowAllInProduction,
74+
);
75+
productionEvents.record(
76+
'FEEDBACK_SUBMITTED',
77+
parameters: {'comment': 'The search result was not useful.'},
78+
);
79+
```
80+
81+
This policy can retain feedback, search terms, URLs, IDs, and other user
82+
content. Hosts must confirm consent, privacy, access, and retention rules before
83+
they use it. The SDK still deep-copies JSON-safe values and applies its hard
84+
JSON and size bounds.
85+
86+
Network routes must be absolute path templates. The SDK drops resolver output
87+
containing a scheme, query, fragment, percent-encoded data, a network-path
88+
prefix, backslash, or whitespace/control characters; host resolvers must still
89+
replace dynamic IDs with placeholders.
6790
HTTP response bodies are retained only when `statusCode >= 400`. JSON and text
6891
are deep-copied and bounded to 16 KiB; binary and unsupported values are
6992
omitted. Successful response bodies are never retained.
@@ -533,8 +556,9 @@ sink registration API has not been published.
533556
output.
534557
- Nested navigator and anonymous-route identity depends on structural fallback
535558
and needs app-specific validation.
536-
- The package captures no logs, network traffic, analytics events, or native
537-
performance signals.
559+
- The package captures no logs or native performance signals. It supports
560+
opt-in external events and network observations with the privacy boundaries
561+
described above.
538562

539563
See [Collector integration](../../docs/integration/collector.md) and
540564
[Capture and fingerprint status](../../docs/design/capture-and-fingerprint.md)

packages/tugboat/example/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ resolution: workspace
3232
dependencies:
3333
flutter:
3434
sdk: flutter
35-
tugboat: ^0.6.0
35+
tugboat: ^0.7.0
3636

3737
# The following adds the Cupertino Icons font to your application.
3838
# Use with the CupertinoIcons class for iOS style icons.

packages/tugboat/lib/src/external_event.dart

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ enum TugboatParameterCaptureMode {
77
namesOnly,
88
allowList,
99
transform,
10-
allowAll;
10+
allowAll,
11+
allowAllInProduction;
1112

1213
String get wireName => switch (this) {
1314
TugboatParameterCaptureMode.namesOnly => 'names_only',
1415
TugboatParameterCaptureMode.allowList => 'allow_list',
1516
TugboatParameterCaptureMode.transform => 'transform',
16-
TugboatParameterCaptureMode.allowAll => 'allow_all',
17+
// Both retain-all modes share the wire label; production scope is carried
18+
// by the mode itself, not a parallel flag.
19+
TugboatParameterCaptureMode.allowAll ||
20+
TugboatParameterCaptureMode.allowAllInProduction => 'allow_all',
1721
};
1822
}
1923

@@ -27,7 +31,8 @@ class TugboatParameterDrop {
2731
///
2832
/// Parameter keys may be captured by default. Values are captured only through
2933
/// an explicit allow-list, transform, or the deliberately named [allowAll]
30-
/// exploration escape hatch.
34+
/// exploration escape hatch. [allowAllInProduction] is a separate production
35+
/// opt-in.
3136
class TugboatParameterPolicy {
3237
const TugboatParameterPolicy._({
3338
required this.mode,
@@ -67,6 +72,18 @@ class TugboatParameterPolicy {
6772
mode: TugboatParameterCaptureMode.allowAll,
6873
);
6974

75+
/// Production opt-in that retains all JSON-safe values within hard limits.
76+
///
77+
/// This can capture feedback, search terms, URLs, IDs, and other user
78+
/// content. The host must confirm consent, privacy, access, and retention
79+
/// rules before it uses this policy.
80+
///
81+
/// Unlike [allowAll], this policy retains values in production capture
82+
/// profiles. It still applies all parameter safety and size limits.
83+
static const allowAllInProduction = TugboatParameterPolicy._(
84+
mode: TugboatParameterCaptureMode.allowAllInProduction,
85+
);
86+
7087
/// Sentinel for transform callbacks.
7188
static const drop = TugboatParameterDrop._();
7289

@@ -261,7 +278,8 @@ _ValueDecision _decideTopLevelValue(
261278
key,
262279
value,
263280
),
264-
TugboatParameterCaptureMode.allowAll => _KeepValue(value),
281+
TugboatParameterCaptureMode.allowAll ||
282+
TugboatParameterCaptureMode.allowAllInProduction => _KeepValue(value),
265283
};
266284
}
267285

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
// Keep this in sync with packages/tugboat/pubspec.yaml. The SDK version test
22
// reads pubspec.yaml directly so release bumps fail fast if this drifts.
3-
const tugboatSdkVersion = '0.6.0';
3+
const tugboatSdkVersion = '0.7.0';

packages/tugboat/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: tugboat
22
description: >-
33
Screenshot-based session replay with compact interaction anchors for Tugboat.
4-
version: 0.6.0
4+
version: 0.7.0
55
repository: https://github.com/blendto/tugboat-flutter
66
issue_tracker: https://github.com/blendto/tugboat-flutter/issues
77
homepage: https://github.com/blendto/tugboat-flutter

packages/tugboat/test/external_event_and_network_test.dart

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,82 @@ void main() {
140140
});
141141
});
142142

143+
testWidgets('production opt-in retains bounded JSON-safe parameter values', (
144+
tester,
145+
) async {
146+
await _pumpCapture(
147+
tester,
148+
config: _testConfig.copyWith(
149+
profile: TugboatCaptureProfile.productionLean,
150+
),
151+
);
152+
153+
TugboatReplay.eventHook(
154+
parameterPolicy: TugboatParameterPolicy.allowAllInProduction,
155+
).record('SEARCH', parameters: {'query': 'private search', 'page': 2});
156+
157+
final event = TugboatReplay.controller!.session!.events.singleWhere(
158+
(event) => event.type == 'external_event',
159+
);
160+
expect(event.data['parameterKeys'], ['query', 'page']);
161+
expect(event.data['parameters'], {'query': 'private search', 'page': 2});
162+
expect(event.data['capture'], {
163+
'values': 'allow_all',
164+
'truncated': false,
165+
'droppedCount': 0,
166+
});
167+
});
168+
169+
testWidgets('exploration capture retains allow-all parameter values', (
170+
tester,
171+
) async {
172+
await _pumpCapture(tester);
173+
174+
TugboatReplay.eventHook(
175+
parameterPolicy: TugboatParameterPolicy.allowAll,
176+
).record('SEARCH', parameters: {'query': 'private search'});
177+
178+
final event = TugboatReplay.controller!.session!.events.singleWhere(
179+
(event) => event.type == 'external_event',
180+
);
181+
expect(event.data['parameterKeys'], ['query']);
182+
expect(event.data['parameters'], {'query': 'private search'});
183+
expect(event.data['capture'], {
184+
'values': 'allow_all',
185+
'truncated': false,
186+
'droppedCount': 0,
187+
});
188+
});
189+
190+
test('effectiveFor only downgrades exploration allowAll', () {
191+
expect(
192+
TugboatParameterPolicy.allowAll.effectiveFor(
193+
TugboatCaptureProfile.productionLean,
194+
),
195+
same(TugboatParameterPolicy.namesOnly),
196+
);
197+
expect(
198+
TugboatParameterPolicy.allowAllInProduction.effectiveFor(
199+
TugboatCaptureProfile.productionLean,
200+
),
201+
same(TugboatParameterPolicy.allowAllInProduction),
202+
);
203+
expect(
204+
TugboatParameterPolicy.allowAllInProduction.mode,
205+
TugboatParameterCaptureMode.allowAllInProduction,
206+
);
207+
expect(
208+
TugboatParameterPolicy.allowAllInProduction.captureValues,
209+
'allow_all',
210+
);
211+
expect(
212+
TugboatParameterPolicy.allowAll.effectiveFor(
213+
TugboatCaptureProfile.exploration,
214+
),
215+
same(TugboatParameterPolicy.allowAll),
216+
);
217+
});
218+
143219
testWidgets('external event ignores active action window', (tester) async {
144220
await _pumpCapture(tester);
145221
final controller = TugboatReplay.controller!;

packages/tugboat_dio/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## 0.7.0
2+
3+
### Changed
4+
5+
- Compatibility release for `tugboat` 0.7.0. The Dio adapter has no runtime
6+
behavior changes.
7+
18
## 0.6.0
29

310
### Added

0 commit comments

Comments
 (0)