| name | Hidden From Reporting Tag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| overview | Add a `$Hidden From Reporting` standard data tag and make all reporting surfaces treat tagged fields as if they were deleted (not present in the dictionary). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| todos |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isProject | false |
Hidden From Reporting Tag
When a CustomerData::Field is hard-deleted, these four independent field-lookup caches stop returning it, which causes all reporting surfaces to exclude the field:
flowchart TD
subgraph fieldSources [Field Lookup Caches]
A["MarketingDataAvailability\n#marketing_data_field_for_type_slot\ndict.fields.index_by(&:internal_name)"]
B["MarketingDataHelper\n#setup_for_dictionary\ndict.fields.index_by(&:internal_name)"]
C["DetailReportCustomerData\n#customer_data_field_for_type_index\ndict.fields.build_hash"]
D["SummaryReportCustomerData\n#dictionary_fields\ndict.fields.to_a"]
end
subgraph consumers [Reporting Surfaces]
E[ColumnCatalog / ReportAdapterCatalog\nES + warehouse reports]
F[Legacy Detail Reports]
G[Legacy Summary Reports]
H[GraphQL reportQueryColumns]
end
A --> E
B --> E
B --> H
C --> F
D --> G
Our goal: make tagged fields disappear from these same four caches without actually deleting them.
Create a new catalog registration file following the exact pattern of existing tags like $Lead and $Consumer Property.
- New file:
packs/standard_data/catalog/hidden_from_reporting_tag.rb - Register one
StandardData::TagDefinition:tag_name: '$Hidden From Reporting'data_type: :any(applies to all field types, like$Consumer Property)description:anddisplay_name:as appropriate
- The
CatalogLoaderauto-discovers*.rbinpacks/standard_data/catalog/, so no wiring needed.
Add one ActiveRecord scope and one dictionary convenience method, then update the four cache-building call sites.
In packs/customer_data/app/models/customer_data/field.rb, add a general-purpose complement to the existing tagged_as scope. While tagged_as returns fields that have ALL of the specified tags, not_tagged_as excludes fields that have ANY of the specified tags:
# @param tag_names [Array<String>] tag names to exclude
# @return [ActiveRecord::Relation] Returns fields that have NONE of the specified tags
def not_tagged_as(*tag_names)
return all if tag_names.empty?
where.not(
id: joins(:tags).where(customer_data_tags: { name: tag_names }).select(:id)
)
endThis uses a NOT IN subquery on the existing HABTM :tags association, so it needs no new indexes (the join table PK is (field_id, tag_id) and customer_data_tags has a unique index on (dictionary_id, name)).
scope :visible_in_reporting, -> { not_tagged_as('$Hidden From Reporting') }This keeps the reporting-specific logic as a thin, readable wrapper over the reusable not_tagged_as primitive.
In packs/customer_data/app/models/customer_data/dictionary.rb, add:
def reportable_fields
fields.visible_in_reporting
endEach site changes dict.fields to dict.reportable_fields:
| File | Method | Change |
|---|---|---|
| marketing_data_availability.rb | marketing_data_field_for_type_slot (line 69) |
dict.fields.index_by -> dict.reportable_fields.index_by |
| marketing_data_helper.rb | setup_for_dictionary (line 19) |
marketing_data_dictionary.fields.index_by -> marketing_data_dictionary.reportable_fields.index_by |
| detail_report_customer_data.rb | customer_data_field_for_type_index (line 407) |
dict.fields.build_hash -> dict.reportable_fields.build_hash |
| summary_report_customer_data.rb | dictionary_fields (line 374) |
effective_customer_dictionary.fields.to_a -> effective_customer_dictionary.reportable_fields.to_a |
These four changes make tagged fields disappear from all downstream reporting surfaces (ColumnCatalog, ReportAdapterCatalog, GraphQL reportQueryColumns, legacy detail/summary reports, ES queries) because every one of those consumers gets its field information from one of these four caches.
- Management UI (
ManageCustomerDataGraphQL resolvers): still usesdictionary.fieldsdirectly -- hidden fields remain visible and editable. LookupFieldCatalog(lookup tables / spreadsheets): uses@dictionary.fields-- lookup tables can still reference hidden fields. (Decide if this should also filter.)- Consumer profile import (
tagged_as("$Consumer Property")): unrelated tag, unaffected. - Data ingest / JS tag / call processing: writes to fields by slot regardless of tags.
Create a post-deploy script to tag specific fields. The mechanism uses the existing Field#standard_data_tag_names= API:
field.standard_data_tag_names = field.standard_data_tag_names.to_a + ['$Hidden From Reporting'](Which fields to tag is a separate decision -- the script template will be ready.)
- Unit:
CustomerData::Fieldspec fornot_tagged_as-- excludes fields with any of the specified tags, returns all when no tags given - Unit:
CustomerData::Fieldspec forvisible_in_reportingscope -- field with/without the$Hidden From Reportingtag - Unit:
CustomerData::Dictionaryspec forreportable_fields - Unit:
MarketingDataAvailabilityspec -- field tagged$Hidden From Reportingreturns nil frommarketing_data_field_available - Unit:
MarketingDataHelperspec --setup_for_dictionaryexcludes hidden field from@fields_hash - Integration: Detail/summary report specs -- hidden field columns are not visible
- Catalog:
StandardData::Catalogspec --$Hidden From Reportingtag is registered
- Should
LookupFieldCatalogalso exclude hidden fields? Currently it iterates@dictionary.fieldsfor lookup table column pickers. If yes, same one-line change. - Should the management UI indicate a field is hidden from reporting? The GraphQL
customerDataFieldsresolver could expose tag info for UI treatment.