Skip to content

feat(webapp,server): per-user opt-out + transparency surface for Slack monitoring #1270

@FelixTJDietrich

Description

@FelixTJDietrich

Part of #1205.

What ships

Two surfaces that together give a monitored user complete legibility and control over Slack channel monitoring — shipped as one because the transparency surface IS the control surface (the opt-out lives inside it).

Per-user opt-out from Slack-sourced feedback (composes against the per-actor opt-out scope locked at #1213):

Transparency surface for monitored Slack channels (composes against the contributor transparency framework from #1213):

  • Webapp account / privacy page renders a "Monitored Slack channels" section listing every channel currently monitored for each workspace the user is in: channel name, workspace, retention window, the admin who allow-listed it, allow-list date, and the user's current opt-out state.
  • Each row links to a per-channel detail view: announce template + user's last 10 findings from this channel.
  • Slack-side: DMing the bot transparency (or near-synonyms) returns the same content as a Block Kit response.
  • The surface answers four legibility questions per channel (Hoel et al. 2018 transparency-to-the-observed): What is monitored? Who decided? How long is it kept? How can I stop it?

Why

The privacy epic locks "opt-out suppresses delivery, not generation or storage" as the per-actor opt-out scope. The user needs that lever, and the Slack-side button on the very DM they want to stop is the autonomy-supportive design. Monitoring chat without making the monitoring visible to the user is the failure mode this epic fights against — the transparency surface is the legibility surface, and it is also the surface that makes the DPIA defensible. Shipping the opt-out and the transparency surface together prevents one shipping without the other, which is exactly the failure mode the literature warns against.

Acceptance criteria

  • user_workspace_preference.suppress_slack_sourced_feedback column exists via Liquibase changeset; default false
  • The DM composer (feat(server): multi-actor practice subscriptions on Slack events #1268) consults the preference before delivery; suppressed users see no DM; the finding row remains in practice_finding; practice_finding_delivery.status=suppressed_by_user_opt_out
  • Webapp account settings exposes one toggle per workspace; saving writes the preference; integration test exercises the toggle + asserts suppression on next finding
  • Slack DM-side: every chat-sourced finding DM carries a "Pause Slack-sourced feedback for this workspace" Block Kit button; clicking writes the preference + posts an ephemeral confirmation
  • Toggle-off (resume) restores normal delivery; toggle is reversible without admin action; findings produced during the opt-out window remain in suppressed_by_user_opt_out permanently (readable via subject-access export, not via DM backfill)
  • Webapp account / privacy page renders a "Monitored Slack channels" section; integration test loads the page for a user in two workspaces with three monitored channels each and asserts all six rows render with correct metadata
  • Each row links to a per-channel detail view showing the announce template + the user's last 10 findings from this channel
  • Slack-side transparency DM returns parallel content as a Block Kit response; integration test asserts block count + content
  • The four legibility questions are explicitly answered per channel row; a content test asserts each row's serialized payload contains the four field names
  • Both surfaces reflect opt-out state changes within one minute (no caching > 60s)
  • A workspace with zero monitored Slack channels renders the section as "No Slack channels are currently monitored for this workspace" rather than an empty block
  • The surface is registered with the contributor transparency framework from feat(webapp,server): privacy admin UI with canonical opt-out scope #1213's slot model for kind=SLACK; ArchUnit test asserts no Slack-specific transparency UI lives outside the framework's slot
  • A metric chat_finding.delivery.suppressed_by_user_opt_out{workspace_id} is exposed via feat(server): per-integration health endpoint and structured-log MDC #1217
  • The subject-access export from feat(server): subject-access export (CSV + JSON) via admin impersonation #1214 includes opt-out-suppressed findings, marked delivery_suppressed_by_user

Tests to write

Implementation notes

  • The preference is per-workspace because a user in two workspaces may consent to Slack-sourced feedback in one and not the other; per-source consent specificity is the GDPR Art. 7 requirement reflected in the rest of the epic.
  • The Slack DM button writes through a server-side endpoint authenticated via Slack's request signing + the user's identity_link lookup; an ArchUnit test asserts the endpoint does not trust a Slack user_id from the request body — only the verified payload.
  • The opt-out does not disable backfill ingestion or live event normalization; events still become IntegrationEvent rows and feed practice detection. Opt-out is at the delivery surface only.
  • The webapp transparency surface is rendered server-side via /api/me/transparency/slack; webapp and Slack DM handler consume the same endpoint shape.
  • The Slack transparency DM handler is registered on the per-workspace Bolt App from refactor(server): per-workspace Bolt App factory replacing the singleton #1244; the trigger string matches case-insensitively + handles a few common phrasings through a small allow-list of synonyms.
  • The detail view's "last 10 findings" reads practice_finding filtered by data.source_channel = <channel_id>; for findings produced before source-channel attribution was added, the row renders as "older finding — channel not recorded".
  • The Block Kit button's action_id is stable + namespaced (hephaestus:scm:opt-out:<workspace_slug>) so the Slack interaction handler routes it predictably.
  • The transparency surface lists every monitored channel the user is a member of, even channels they've never posted in — the channel is monitored regardless of the user's participation; the user has a right to know.

Dependencies

Depends on #1268, #1275, #1213, #1214, #1244.

Metadata

Metadata

Assignees

No one assigned

    Labels

    application-serverSpring Boot server: APIs, business logic, databasefeatureNew feature or enhancementpriority:highAddress this sprint - Significant impactwebappReact app: UI components, routes, state management

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions