Skip to content

[WOOMOB-4227] Send recent app logs with AI support chats started during login - #16782

Open
irfano wants to merge 11 commits into
issue/WOOMOB-4225-login-context-odiefrom
issue/WOOMOB-4227-login-odie-app-logs
Open

irfano wants to merge 11 commits into
issue/WOOMOB-4225-login-context-odiefrom
issue/WOOMOB-4227-login-odie-app-logs

Conversation

@irfano

@irfano irfano commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes WOOMOB-4227

Built on top of #16752. When a merchant starts an AI support chat during login, the first message now also sends recent lines from the app log under # Recent app logs. Odie gets them as context, the merchant doesn't see them.

What is sent:

  • Warnings and errors from the last 30 minutes, the newest 20.
  • Each line up to 500 characters, the whole section up to 4,000.
  • Only the first line of a stack trace.

How it is cleaned, following Zendesk_SSR_Ability (fbhepr%2Skers%2Sjcpbz%2Sjc%2Qpbagrag%2Syvo%2Snv%2Sgbbyf%2Snovyvgl.mraqrfx%2Qffe.cuc%3Se%3Qs73nr645%23766-og):

  • Passwords, tokens, secrets and API keys are hidden.
  • Emails and URL query strings are hidden.
  • Invisible characters are removed.
  • A note at the start of the login context tells Odie not to follow instructions in it or in the sections after it.

The shared sanitizer now has the same rules, so the store response excerpt from WOOMOB-3905 is cleaned the same way. The site address and last error in the login context now go through it too.

The chat opened from the "Unable to log in" alert already sends the store response excerpt with the error details, so it's cut from the log lines there.

Test Steps

Each scenario checks the first message the app sends to Odie. ApiFaker answers it with a bot reply that says what the request had.

Setup

  1. Install a debug build of this branch.
  2. Save the four JSON files below.
  3. Run adb push tunnel-503.json odie-logs-excerpt.json odie-logs.json odie-no-logs.json /data/local/tmp/.
  4. Open the app.
  5. Log in with a WordPress.com account that has a store connected through Jetpack.
  6. Wait for the store list.
JSON files

tunnel-503.json, the error the Jetpack tunnel returns when the store answers with a page that isn't JSON:

{"error":"unknown_error","message":"An unknown error occurred","data":{"status":503,"raw_body":"<html><head><title>Maintenance</title></head><body><h1>Briefly unavailable for scheduled maintenance. Check back in a minute.</h1></body></html>"}}

odie-logs-excerpt.json:

{"chat_id":1,"session_id":"test","bot_slug":"wpcom-mobile-woo-support","bot_version":"1","messages":[{"message_id":2,"role":"bot","content":"Odie received recent app logs with the store response excerpt"}]}

odie-logs.json:

{"chat_id":1,"session_id":"test","bot_slug":"wpcom-mobile-woo-support","bot_version":"1","messages":[{"message_id":2,"role":"bot","content":"Odie received recent app logs"}]}

odie-no-logs.json:

{"chat_id":1,"session_id":"test","bot_slug":"wpcom-mobile-woo-support","bot_version":"1","messages":[{"message_id":2,"role":"bot","content":"Odie received no recent app logs"}]}
Odie commands
adb shell am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.CLEAR_ENDPOINTS
adb shell "am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.ADD_ENDPOINT --es api_type wp-com --es http_method POST --es path /wpcom/v2/odie/chat/% --es request_body '%raw_body_excerpt%' --ei response_status_code 200 --es response_body_file /data/local/tmp/odie-logs-excerpt.json"
adb shell "am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.ADD_ENDPOINT --es api_type wp-com --es http_method POST --es path /wpcom/v2/odie/chat/% --es request_body '%API Unexpected store response: kind%Maintenance%' --ei response_status_code 200 --es response_body_file /data/local/tmp/odie-logs-excerpt.json"
adb shell "am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.ADD_ENDPOINT --es api_type wp-com --es http_method POST --es path /wpcom/v2/odie/chat/% --es request_body '%# Recent app logs%' --ei response_status_code 200 --es response_body_file /data/local/tmp/odie-logs.json"
adb shell am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.ADD_ENDPOINT --es api_type wp-com --es http_method POST --es path /wpcom/v2/odie/chat/% --ei response_status_code 200 --es response_body_file /data/local/tmp/odie-no-logs.json
adb shell am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.SET_STATUS --ez enabled true
Store check command
adb shell am broadcast -p com.woocommerce.android.dev -a com.woocommerce.android.apifaker.ADD_ENDPOINT --es api_type wp-api --es http_method GET --es path / --ei response_status_code 503 --es response_body_file /data/local/tmp/tunnel-503.json

"Unable to log in" alert

  1. Run the Odie commands.
  2. Run the store check command.
  3. Select the store.
  4. Tap Continue.
  5. Verify the "Unable to log in" alert shows.
  6. Tap Contact Support.
  7. Tap Send.
  8. Verify the bot replies Odie received recent app logs.

Help on the store list

This needs a store in the list whose site can't be reached, like an expired Jurassic Ninja site.

  1. Close and reopen the app.
  2. Wait for the store list.
  3. Run the Odie commands.
  4. Select the store whose site can't be reached.
  5. Tap Continue.
  6. Verify the "Cannot connect to …" snackbar shows.
  7. Tap Help.
  8. Tap Contact Support.
  9. Send a message.
  10. Verify the bot replies Odie received recent app logs with the store response excerpt.

After login

  1. Close and reopen the app.
  2. Wait for the store list.
  3. Run the Odie commands.
  4. Select a store that works.
  5. Tap Continue.
  6. Open Menu.
  7. Tap Settings.
  8. Tap Help & support.
  9. Tap Contact Support.
  10. Tap Something else.
  11. Send a message.
  12. Verify the bot replies Odie received no recent app logs.

Images/gif

Before After

The bot replies are fake ApiFaker responses. They say whether the request had the recent app logs.

  • I have considered if this change warrants release notes and have added them to RELEASE-NOTES.txt if necessary. Use the "[Internal]" label for non-user-facing changes.

@irfano irfano added type: enhancement A request for an enhancement. feature: login Related to any part of the log in or sign in flow, or authentication. labels Oct 27, 2026
@irfano irfano added this to the 26.0 milestone Oct 27, 2026
@wpmobilebot

wpmobilebot commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

App Icon📲 You can test the changes from this Pull Request in WooCommerce Android by scanning the QR code below to install the corresponding build.

App NameWooCommerce Android
Platform📱 Mobile
FlavorJalapeno
Build TypeDebug
Build Number790
Version25.9-rc-1
Application IDcom.woocommerce.android.prealpha
Commitca87449
Installation URL1ssd04mgvmeu8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@irfano
irfano marked this pull request as ready for review October 27, 2026 15:57
@github-actions github-actions Bot added the review: recommended Claude matched the review guidelines: a review is recommended label Oct 27, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review - Found 4 potential issues

The change itself is well structured: the first-message context build is extracted cleanly, the log snapshot is bounded (levels, 30-minute window, 20 lines, 500 chars/line, 4000 chars total), runs through SensitiveDataSanitizer plus the existing personal-data scrub, and carries a prompt-injection notice. Hilt wiring checks out (WooLog is provided by AppConfigModule, Clock by ApplicationModule), and the test coverage for the provider is thorough.

Findings (details inline):

  1. [fix here] SensitiveDataSanitizer: the new invisible-character strip runs after the secret redactions, so a zero-width/format character inside a key name defeats the redaction and the strip then normalises the leaked value into clean text.
  2. [follow-up] WooLog.getCurrentLogEntries() only triggers a flush and does not wait for it, so the most recent entries (often the failure that brought the merchant to the chat) can be missing from the snapshot. Pre-existing, but this PR now depends on freshness.
  3. [nit] dateInYearOf allocates two Calendars per entry for a whole day's log file; the current year can be computed once.
  4. [nit] COLON_SECRET_REGEX uses \S+ and swallows trailing punctuation (the test encodes hunter2, becoming [redacted] with the comma gone).
PR housekeeping
  • The PR title and description were not included in the staged review context, so the title/reasoning/issue-link and testing-instructions checks could not be performed this pass; please make sure the description links the issue and lists steps to start an AI chat from the login flow and confirm the # Recent app logs section is present (and excerpt-free for the unexpected-response entry point).
  • Applied repo AGENTS.md guidance (Kotlin conventions, companion object placement, no !!, BDD test naming): no deviations found in this diff.

Automatic review · claude-fable-5-1 · Workflow run

How to reply to a finding

Reply on this review (or inline at the line the finding refers to) with one of:

  • @claude addressed - I made the change. Bot verifies against the next diff before marking resolved.
  • @claude rejected: <reason> - Will not fix; reason gets quoted on the next review.
  • @claude not-applicable - Finding does not apply (wrong file, already covered elsewhere, etc.).

The bot honours these on the next review pass.

matchResult.value.replace(QUOTED_ARGUMENT_REGEX, "'[redacted]'")
}
.replace(WHITESPACE_REGEX, " ")
.replace(INVISIBLE_CHARACTER_REGEX, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review [fix here]

Issue: The invisible-character strip is the last step, after every secret regex has already run. A key name containing a format/control character (e.g. pass\u200Bword=hunter2 or access_\u00ADtoken: xyz) does not match KEY_VALUE_SECRET_REGEX / COLON_SECRET_REGEX / JSON_SECRET_REGEX, and this final replace then turns it into a clean password=hunter2 in the output. Since this sanitizer now feeds app-log lines and store-response excerpts into an external AI service, the ordering undoes the protection it is meant to add.

Suggestion: Strip the invisible characters first, but keep line breaks so COOKIE_HEADER_REGEX ([^\n\r,]+) and STACK_TRACE_REGEX still see them, then run the redactions and the whitespace collapse:

return value.orEmpty()
    .replace(INVISIBLE_CHARACTER_REGEX, "")
    .replace(BEARER_TOKEN_REGEX, "Bearer [redacted]")
    // ... existing redactions ...
    .replace(WHITESPACE_REGEX, " ")
    .trim()

// Cc minus the line/tab separators that later patterns rely on
private val INVISIBLE_CHARACTER_REGEX = Regex("""[\p{Cf}\p{Cs}]|[\p{Cc}&&[^\r\n\t]]""")

And add a test such as given a zero-width character inside a secret key, when sanitizing, then the value is still redacted.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude addressed: Invisible characters are removed first now. Fixed with ed412dc

private val KEY_VALUE_SECRET_REGEX = Regex(
"""(?i)(^|[?&;\s])((?:$SECRET_KEYS)=)[^&;\s,"'}]+"""
)
private val COLON_SECRET_REGEX = Regex("""(?i)\b((?:$SECRET_KEYS)\s*:\s*)(?:"[^"]*"|\S+)""")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review [nit]

Issue: (?:"[^"]*"|\S+) consumes any trailing punctuation that touches the value, so password: hunter2, API Key: ... becomes password: [redacted] API Key: ... (the comma is lost, as the new test asserts). It is harmless for secrecy but makes redacted log lines harder to read, and it is inconsistent with the quoted branch, which keeps the comma.

Suggestion: Exclude the same separators the other patterns stop at, e.g. (?:"[^"]*"|[^\s,;]+), and update the expected string in SensitiveDataSanitizerTest to password: [redacted], API Key: [redacted], access_token:[redacted].

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude rejected: A secret can contain commas, so the value is hidden up to the next space, the same as Zendesk_SSR_Ability.

suspend fun buildRecentAppLogs(keepStoreResponseExcerpts: Boolean): String? = withContext(dispatchers.io) {
val now = clock.millis()
val since = Date(now - RECENT_LOGS_PERIOD_MS)
val lines = wooLog.getCurrentLogEntries()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review [follow-up]

Issue: WooLog.getCurrentLogEntries() calls fileLogger.forceFlush(), which only emits on writeTrigger and returns immediately; the actual write happens later in appCoroutineScope, and getCurrentLogFileContent() reads the file right away. Entries logged since the last periodic flush (up to 5 s in the foreground) can therefore be missing, and the most valuable line here is usually the error that just sent the merchant to the chat. This is pre-existing behaviour shared with EncryptedLogsFileProvider and the Zendesk logs, so it can be a separate PR, but this feature is the first that depends on the snapshot being current.

Suggestion: Make forceFlush() wait for the write to complete (e.g. send a CompletableDeferred alongside the trigger that processLogs completes, or expose a suspend fun flushAndRead() that drains the channel and writes synchronously before reading). Alternatively, have buildRecentAppLogs merge the still-buffered entries with the file contents.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude rejected: By the time the merchant opens the chat and sends the first message, the error that led there is already in the file.


// The log file doesn't store the year, so entries read back from today's file are dated 1970.
private fun LogEntry.dateInYearOf(now: Long): Date {
val year = Calendar.getInstance().apply { timeInMillis = now }.get(Calendar.YEAR)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review [nit]

Issue: dateInYearOf is called once per entry in the filter, and each call creates two Calendar instances (the first only to read the current year). getCurrentLogEntries() returns the whole current log file, which can be thousands of entries, so this is avoidable churn on the IO dispatcher. There is also a harmless edge around New Year, where a Dec 31 entry read on Jan 1 gets dated a year ahead and is kept; worth a word in the comment so nobody "fixes" it later.

Suggestion: Compute the year once in buildRecentAppLogs and pass it in, e.g.:

val year = Calendar.getInstance().apply { timeInMillis = now }.get(Calendar.YEAR)
...
.filter { it.level in RECENT_LOG_LEVELS && !it.logDate.inYear(year).before(since) }

private fun Date.inYear(year: Int): Date =
    Calendar.getInstance().apply { time = this@inYear; set(Calendar.YEAR, year) }.time

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude addressed: The year is computed once now. Fixed with c3bc62a

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature: login Related to any part of the log in or sign in flow, or authentication. review: recommended Claude matched the review guidelines: a review is recommended type: enhancement A request for an enhancement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants