
Problem / motivation
Debug.action(...) and Debug.error(...) emit log lines that carry the verb (CLICK, TYPE, PASTE, KEY_DOWN, ...) followed by the exact arguments passed to the primitive — screen coordinates, typed text, keystroke sequences. That is the ideal debug surface at development time and a real privacy hazard the moment those logs are shipped to a central collector, an artifact server, or a customer-facing terminal.
Concrete cases that surfaced recently:
- CI run captures
TYPE("password-in-plain") in the archive, artifact stays on the build server for the retention window
- Live demo on a client site with the console visible on the projected screen, verbose logs leaking business values in front of the audience
- Long-running background OCR loops that flood stderr with thousands of identical
HOVER(1234, 567) lines nobody wants to read
Today the only knob is verbosity (Debug level 0..5). Nothing at the shape or content level. Either the log is fully verbose or nothing at all, which is a binary that fits neither production CI nor demos.
Proposed solution
Introduce a small renderer org.sikuli.support.ActionLogRenderer that sits on the emission path of Debug.action() and Debug.error(), governed by a new Settings.ActionLogMode (case-insensitive String for max flexibility, defaults to "CLEAR"):
| Mode |
Behaviour |
CLEAR |
Vanilla, verbatim message and arguments. Same as today. |
MASKED |
Keeps the verb (CLICK, TYPE, PASTE), replaces every argument value with ******. Verb-level observability preserved, zero value leakage. |
SILENT |
Emits nothing. The log is at peace. |
Wiring is a single injection point in each of Debug.action(...) and Debug.error(...):
String rendered = org.sikuli.support.ActionLogRenderer.render(
message, args, Settings.ActionLogMode, /*isError=*/ false);
if (rendered == null) return; // SILENT swallows the line
log(-1, actionPrefix, rendered);
Fail-safe by design: any rendering exception is swallowed and the raw message goes through — the log layer must never break the caller's script. SILENT returns null and the surrounding log(...) call is skipped entirely (no empty-line ghost in the stream).
Alternatives considered
- Reuse
Debug verbosity level (0..5) — solves nothing at the shape level. A Debug 5 line still contains the same sensitive args, just among more lines.
- Redact at the log collector side — pushes the problem downstream, requires each customer to maintain a redaction rule set, and does not help live demos or scripts that never reach a collector.
- Ship a
@Sensitive annotation for individual args and mask by annotation — surgical but requires touching every primitive signature; over-engineered for the 90 % case where the answer is "mask everything in production, verbose everything in dev".
Where would this live?
- API (Screen / Region / Pattern / Match)
API impact
Yes — new public API (backward-compatible):
Settings.ActionLogMode (public static field, defaults to "CLEAR")
org.sikuli.support.ActionLogRenderer.render(...) (public static utility method)
Callers of Debug.action(...) and Debug.error(...) do not change. Default behaviour is unchanged for any existing script that never touches the new field.
Use case / impact
- Production CI —
Settings.ActionLogMode = "MASKED" at the start of every run, no credentials in the archived logs, verb-level trace still available for post-mortem.
- Live demo / client site —
MASKED or SILENT depending on how much of the console the audience sees.
- Long-running background OCR loops —
SILENT on the noisy branch, promote back to CLEAR for the assertion-carrying calls only.
- Dev / debug —
CLEAR stays the default, nothing changes.
Rollout is a one-line pin in an Init.java or equivalent bootstrap; downstream users who never set the field see the current behaviour byte for byte.
Additional context
The renderer is intentionally extensible — Mode.fromName(String) accepts any case-insensitive alias and the rendering path is table-driven, so future modes can be added without touching the wiring in Debug. What ships in v1 are the three documented modes above; the surface is designed to grow.
Rationale for String in Settings.ActionLogMode rather than an enum: Settings fields are read via reflection by several script runners (Jython, JRuby) and used from settings.properties; a String field roundtrips cleanly through all of them without a per-runner adapter, and Mode.fromName(...) handles the parse once at render time.
The three modes above are the ones I recommend for production. If you read the source, you will find more. Whether you use them in a demo is a matter of taste. 🤡
🦎
Problem / motivation
Debug.action(...)andDebug.error(...)emit log lines that carry the verb (CLICK,TYPE,PASTE,KEY_DOWN, ...) followed by the exact arguments passed to the primitive — screen coordinates, typed text, keystroke sequences. That is the ideal debug surface at development time and a real privacy hazard the moment those logs are shipped to a central collector, an artifact server, or a customer-facing terminal.Concrete cases that surfaced recently:
TYPE("password-in-plain")in the archive, artifact stays on the build server for the retention windowHOVER(1234, 567)lines nobody wants to readToday the only knob is verbosity (Debug level 0..5). Nothing at the shape or content level. Either the log is fully verbose or nothing at all, which is a binary that fits neither production CI nor demos.
Proposed solution
Introduce a small renderer
org.sikuli.support.ActionLogRendererthat sits on the emission path ofDebug.action()andDebug.error(), governed by a newSettings.ActionLogMode(case-insensitiveStringfor max flexibility, defaults to"CLEAR"):CLEARMASKEDCLICK,TYPE,PASTE), replaces every argument value with******. Verb-level observability preserved, zero value leakage.SILENTWiring is a single injection point in each of
Debug.action(...)andDebug.error(...):Fail-safe by design: any rendering exception is swallowed and the raw message goes through — the log layer must never break the caller's script.
SILENTreturnsnulland the surroundinglog(...)call is skipped entirely (no empty-line ghost in the stream).Alternatives considered
Debugverbosity level (0..5) — solves nothing at the shape level. ADebug 5line still contains the same sensitive args, just among more lines.@Sensitiveannotation for individual args and mask by annotation — surgical but requires touching every primitive signature; over-engineered for the 90 % case where the answer is "mask everything in production, verbose everything in dev".Where would this live?
API impact
Yes — new public API (backward-compatible):
Settings.ActionLogMode(public static field, defaults to"CLEAR")org.sikuli.support.ActionLogRenderer.render(...)(public static utility method)Callers of
Debug.action(...)andDebug.error(...)do not change. Default behaviour is unchanged for any existing script that never touches the new field.Use case / impact
Settings.ActionLogMode = "MASKED"at the start of every run, no credentials in the archived logs, verb-level trace still available for post-mortem.MASKEDorSILENTdepending on how much of the console the audience sees.SILENTon the noisy branch, promote back toCLEARfor the assertion-carrying calls only.CLEARstays the default, nothing changes.Rollout is a one-line pin in an
Init.javaor equivalent bootstrap; downstream users who never set the field see the current behaviour byte for byte.Additional context
The renderer is intentionally extensible —
Mode.fromName(String)accepts any case-insensitive alias and the rendering path is table-driven, so future modes can be added without touching the wiring inDebug. What ships in v1 are the three documented modes above; the surface is designed to grow.Rationale for
StringinSettings.ActionLogModerather than anenum:Settingsfields are read via reflection by several script runners (Jython, JRuby) and used fromsettings.properties; aStringfield roundtrips cleanly through all of them without a per-runner adapter, andMode.fromName(...)handles the parse once at render time.The three modes above are the ones I recommend for production. If you read the source, you will find more. Whether you use them in a demo is a matter of taste. 🤡
🦎