Skip to content

Add request-body parameter filtering for cassette recordings#1

Merged
danielmbarlow merged 15 commits into
masterfrom
copilot/add-filter-parameters-config
Jun 8, 2026
Merged

Add request-body parameter filtering for cassette recordings#1
danielmbarlow merged 15 commits into
masterfrom
copilot/add-filter-parameters-config

Conversation

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown

Sensitive request fields could be ignored for playback matching (GCR.ignore) but were still persisted verbatim in cassette JSON. This change adds recording-time field filtering so configured values are replaced before they enter GCR::Request and before cassettes are written.

  • Config API: request filtering controls

    • Added GCR.filter_parameters(*fields) to map fields to "[FILTERED]".
    • Added GCR.filter_parameters_with(hash) to map fields to custom replacement values.
    • Added GCR.filtered_parameters as the string-keyed replacement registry.
  • Recording path: sanitize at Request.from_proto

    • GCR::Request.from_proto now parses proto JSON, applies configured replacements, and stores the sanitized JSON body.
    • Filtering happens at capture time (not at serialization), keeping in-memory request objects and cassette payloads consistent.
  • Spec updates

    • Added coverage for:
      • default replacement via filter_parameters
      • custom replacement via filter_parameters_with
      • playback matching when cassette was recorded with filtered values
    • Reset @filtered_parameters in spec/spec_helper.rb before(:each) to prevent cross-test leakage.
  • Docs

    • Updated README.md configuration section with usage for default and custom filtering.
GCR.filter_parameters(:token, :api_key)
GCR.filter_parameters_with(requestId: "[REQUEST_ID]", token: "[TOKEN]")
Original prompt

Summary

Add a filter_parameters configuration option that sanitises sensitive field values before they are written to cassette files on disk. This mirrors the existing ignore pattern but targets recording rather than matching.

Background

Currently GCR.ignore excludes fields from request matching during playback, but sensitive values are still written verbatim to cassette JSON files. We need a way to replace sensitive field values (e.g. tokens, API keys) with a placeholder string before the cassette is persisted.

Changes required

1. lib/gcr.rb — add filter_parameters and filter_parameters_with config methods

Add the following methods to the GCR module, following the same style as the existing ignore / ignored_fields methods:

# Filter these fields when recording requests to cassettes, replacing their
# values with a placeholder string.
#
# *fields - String or Symbol field names to filter (eg. :token, :api_key).
#           Values will be replaced with "[FILTERED]".
#
# Returns nothing.
def filter_parameters(*fields)
  filtered_parameters.merge!(
    fields.flatten.each_with_object({}) { |f, h| h[f.to_s] = "[FILTERED]" }
  )
end

# Filter these fields when recording requests to cassettes, replacing their
# values with custom placeholder strings.
#
# hash - A Hash mapping field names (String or Symbol) to replacement values.
#
# Returns nothing.
def filter_parameters_with(hash)
  filtered_parameters.merge!(hash.transform_keys(&:to_s))
end

# Fields that are filtered when recording requests to cassettes.
#
# Returns a Hash mapping String field names to String replacement values.
def filtered_parameters
  @filtered_parameters ||= {}
end

2. lib/gcr/request.rb — apply filtering in from_proto

Modify GCR::Request.from_proto to sanitise the body before storing it:

def self.from_proto(route, proto_req, *_)
  body = JSON.parse(proto_req.to_json(emit_defaults: true))
  GCR.filtered_parameters.each do |field, replacement|
    body[field] = replacement if body.key?(field)
  end

  new(
    "route"      => route,
    "class_name" => proto_req.class.name,
    "body"       => JSON.dump(body),
  )
end

3. spec/gcr_spec.rb — add tests for the new behaviour

Add tests covering:

  • filter_parameters replaces the specified field value with "[FILTERED]" in the recorded cassette JSON
  • filter_parameters_with replaces the specified field value with a custom string in the recorded cassette JSON
  • Filtered fields are still matched correctly during playback (the cassette was recorded with the filtered value, so playback must also filter the same field to match)

Use the existing requestId field on HelloRequest (used via Greetings::Client.hello("joe", "1")) as the field to filter in tests, since it is already exercised by the fixture.

Also make sure to reset GCR.filtered_parameters (set GCR.instance_variable_set(:@filtered_parameters, nil)) in the before(:each) block in spec/spec_helper.rb alongside the existing resets, so tests don't leak state.

Notes

  • Filtering happens at from_proto time — the sensitive value never enters the in-memory Request object, keeping memory and disk consistent.
  • filter_parameters and ignore are orthogonal: filter_parameters affects what is written to disk; ignore affects matching during playback.
  • The README.md should be updated to document the new configuration option under the ## Configuration section.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: Is there a way of configuring parameter filtering on this repo?

Assistant: Yes — GCR supports a form of parameter filtering via GCR.ignore. It lets you specify field names that are excluded from request matching when playing back cassettes.

How it works

# Ignore these fields when matching requests.
#
# *fields - String field names (eg. "token").
#
# Returns nothing.
def ignore(*fields)
  ignored_fields.concat(fields.map(&:to_s))
end

# Fields that are ignored when matching requests.
#
# Returns an Array of Strings.
def ignored_fields
  @ignored_fields ||= []
end

The ignored fields are then consulted during request comparison in GCR::Request#==:

def ==(other)
  return false unless route == other.route
  return false unless class_name == other.class_name

  parsed_body.keys.all? do |k|
    next true if GCR.ignored_fields.include?(k)
    parsed_body[k] == other.parsed_body[k]
  end
end

Usage example

From the spec, you can see it in action:

GCR.igno...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

@thomasLeclaire

thomasLeclaire commented Jun 8, 2026

Copy link
Copy Markdown

Logo
Checkmarx One – Scan Summary & Details90c53c6e-373f-4c1a-a8ef-a95deead98dd

Great job! No new security vulnerabilities introduced in this pull request


Communicate with Checkmarx by submitting a PR comment with @Checkmarx followed by one of the supported commands. Learn about the supported commands here.

Copilot AI changed the title [WIP] Add filter_parameters configuration for sanitising sensitive fields Add request-body parameter filtering for cassette recordings Jun 8, 2026
Copilot AI requested a review from danielmbarlow June 8, 2026 14:19
@danielmbarlow danielmbarlow changed the base branch from master to google-grpc-compatible June 8, 2026 14:41
@danielmbarlow

Copy link
Copy Markdown

@copilot resolve the merge conflicts in this pull request

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown
Author

@copilot resolve the merge conflicts in this pull request

Resolved. I merged google-grpc-compatible into this branch, fixed the merge conflicts, and pushed commit 3bb1fca.

@danielmbarlow danielmbarlow marked this pull request as ready for review June 8, 2026 15:58
@danielmbarlow danielmbarlow changed the base branch from google-grpc-compatible to master June 8, 2026 15:59

@danielmbarlow danielmbarlow 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.

Includes commits from these feature branches

Effilab:google-grpc-compatible ✔️

digivizer#4

Battle tested in CI for 2 years

Effilab:copilot/add-filter-parameters-config ✔️

Tested locally with

  • grpc (1.78.1)
  • google-ads-googleads (36.1.0)

Test:

  • Record GCR cassette
  • Read the cassette using:
pigz -dc spec/integration/gcr_cassettes/cassette_name.json.zz | jq '.reqs[][]' | grep httpAuthorizationToken

# =>  "body": "{ ...    "httpAuthorizationToken\":\"[FILTERED]\", ...
  • Re-ran test and cassette read correctly

@danielmbarlow danielmbarlow merged commit adb65ed into master Jun 8, 2026
@thomasLeclaire thomasLeclaire deleted the copilot/add-filter-parameters-config branch June 9, 2026 07:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants