[config_parser] Support filtering fields on client

The support for reading field descriptors in golang's protobuf library
requires reflection and the 'descriptor' library. It's gross, but
AFACT it's the *correct* way of doing things.
See: https://github.com/golang/protobuf/issues/358

CB-199 #comment
Fields can now easily be filtered by adding the annotation:

    [(cobalt_options).hide_on_client = true]

Change-Id: Ie96f445fe9a1a26760d171e9b7c2a76b72111422
diff --git a/config/BUILD.gn b/config/BUILD.gn
index 3b8d9b6..e0fe1e0 100644
--- a/config/BUILD.gn
+++ b/config/BUILD.gn
@@ -12,6 +12,7 @@
   proto_in_dir = "//third_party/cobalt"
   sources = [
     "cobalt_config.proto",
+    "annotations.proto",
     "encodings.proto",
     "metric_definition.proto",
     "metrics.proto",
diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt
index d83736c..a4fe73b 100644
--- a/config/CMakeLists.txt
+++ b/config/CMakeLists.txt
@@ -25,14 +25,15 @@
                              false
                              encodings metrics metric_definition
                              report_configs report_definition
-                             cobalt_config window_size project)
+                             cobalt_config window_size annotations project)
 
 # Generate Go bindings for the config .proto files.
 cobalt_protobuf_generate_go(generate_config_pb_go_files
                             CONFIG_PB_GO_FILES
                             false
                             cobalt_config encodings metrics metric_definition
-                            report_configs report_definition window_size)
+                            report_configs report_definition window_size
+                            annotations project)
 
 add_library(buckets_config
             buckets_config.cc
diff --git a/config/annotations.proto b/config/annotations.proto
new file mode 100644
index 0000000..132f23e
--- /dev/null
+++ b/config/annotations.proto
@@ -0,0 +1,18 @@
+// Copyright 2018 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+syntax = "proto3";
+
+package cobalt;
+
+import "google/protobuf/descriptor.proto";
+
+option go_package = "config";
+
+message CobaltOptions {
+  bool hide_on_client = 1;
+}
+
+extend google.protobuf.FieldOptions {
+  CobaltOptions cobalt_options = 50000;
+}
diff --git a/config/config_parser/CMakeLists.txt b/config/config_parser/CMakeLists.txt
index 3373513..f339c86 100644
--- a/config/config_parser/CMakeLists.txt
+++ b/config/config_parser/CMakeLists.txt
@@ -32,7 +32,8 @@
                       ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/project_config.go
                       ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/git.go
                       ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/config_reader.go
-                      ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/id.go)
+                      ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/id.go
+                      ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/filter.go)
 
 set(CONFIG_VALIDATOR_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/config_validator/validator.go
                          ${CMAKE_CURRENT_SOURCE_DIR}/src/config_validator/system_profile_field.go
@@ -69,7 +70,8 @@
 set(CONFIG_PARSER_TEST_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/project_list_test.go
                             ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/config_reader_test.go
                             ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/id_test.go
-                            ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/project_config_test.go)
+                            ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/project_config_test.go
+                            ${CMAKE_CURRENT_SOURCE_DIR}/src/config_parser/filter_test.go)
 
 add_custom_command(OUTPUT ${CONFIG_PARSER_TEST_BIN}
     COMMAND ${GO_BIN} test -c -o ${CONFIG_PARSER_TEST_BIN} ${CONFIG_PARSER_TEST_SRC} ${CONFIG_PARSER_SRC}
diff --git a/config/config_parser/src/config_parser/filter.go b/config/config_parser/src/config_parser/filter.go
new file mode 100644
index 0000000..5900e61
--- /dev/null
+++ b/config/config_parser/src/config_parser/filter.go
@@ -0,0 +1,136 @@
+package config_parser
+
+import (
+	"config"
+	"reflect"
+	"strings"
+
+	"github.com/golang/glog"
+	"github.com/golang/protobuf/descriptor"
+	"github.com/golang/protobuf/proto"
+	descriptor_pb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+)
+
+func FilterHideOnClient(c *config.CobaltConfig) {
+	filterReflectedValue(reflect.ValueOf(c))
+}
+
+func filterWithCobaltOptions(options *config.CobaltOptions, field reflect.Value) {
+	if options.HideOnClient {
+		// Set the field to its zero value (in proto-land this is the same as
+		// unsetting the value).
+		field.Set(reflect.Zero(field.Type()))
+	}
+}
+
+func filterPointer(rc reflect.Value) {
+	if rc.IsNil() {
+		return
+	}
+
+	// get the value of the pointer.
+	msgValue := rc.Elem()
+
+	// Check if we can treat rc as a descriptor.Message
+	msgInterface, ok := rc.Interface().(descriptor.Message)
+
+	if !ok {
+		// This might be a oneof wrapper struct, so we treat it as a generic
+		// struct and blindly recurse through all fields.
+		filterReflectedValue(msgValue)
+		return
+	}
+
+	// The way that golang handles oneof fields does not map 1:1 onto the logical
+	// protobuf structure. It is implemented as a golang interface for each of the
+	// possible variants. Since we can't easily look up the descriptor information
+	// for individual oneof variants, we ignore them and simply try to filter the
+	// value.
+	for i := 0; i < msgValue.NumField(); i++ {
+		if msgValue.Type().Field(i).Tag.Get("protobuf_oneof") != "" {
+			filterReflectedValue(reflect.ValueOf(msgValue.Field(i).Interface()))
+		}
+	}
+
+	// Get message's proto descriptor information
+	_, msgDescriptor := descriptor.ForMessage(msgInterface)
+	for _, field := range msgDescriptor.GetField() {
+		filterMessageField(field, msgValue)
+	}
+}
+
+func findFieldWithinStruct(name string, rc reflect.Value) (foundField int) {
+	foundField = -1
+
+	nameTag := "name=" + name + ","
+	// Search through the struct for a matching field tag (there's no way to
+	// convert field.GetName() to the generated field name, so we're stuck
+	// with this See: https://github.com/golang/protobuf/issues/457)
+	for i := 0; i < rc.NumField(); i++ {
+		// The go protobuf compiler adds tags to the generated go code. One of those
+		// tags is called 'protobuf' and contains key-value entries, one of which is
+		// the name of the field in the proto file.
+		if strings.Contains(rc.Type().Field(i).Tag.Get("protobuf"), nameTag) {
+			foundField = i
+			break
+		}
+	}
+
+	return foundField
+}
+
+func filterMessageField(field *descriptor_pb.FieldDescriptorProto, msgValue reflect.Value) {
+	foundField := findFieldWithinStruct(field.GetName(), msgValue)
+
+	// Oneof fields are not treated the same as other fields, so there will be
+	// a "field" at this level, that has no corresponding real field in the
+	// struct. This means that we have no straightforward way of filtering
+	// individual oneof variants, or a oneof field as a whole. We can filter
+	// within the sub-messages, but at the level of oneof, our handling is subtly
+	// broken.
+	if foundField < 0 {
+		return
+	}
+
+	// Finally, we can extract the CobaltOptions extension.
+	if options_extension, err := proto.GetExtension(field.GetOptions(), config.E_CobaltOptions); err == nil {
+		filterWithCobaltOptions(options_extension.(*config.CobaltOptions), msgValue.Field(foundField))
+	}
+
+	// Recurse for the sub message.
+	filterReflectedValue(msgValue.Field(foundField))
+}
+
+func filterReflectedValue(rc reflect.Value) {
+	switch rc.Kind() {
+	case reflect.Ptr:
+		filterPointer(rc)
+	case reflect.Struct:
+		for i := 0; i < rc.NumField(); i++ {
+			filterReflectedValue(rc.Field(i))
+		}
+	case reflect.Array, reflect.Slice:
+		for i := 0; i < rc.Len(); i++ {
+			filterReflectedValue(rc.Index(i))
+		}
+	case reflect.Map:
+		for _, k := range rc.MapKeys() {
+			filterReflectedValue(rc.MapIndex(k))
+		}
+	case
+		reflect.Uint, reflect.Int,
+		reflect.Uint8, reflect.Int8,
+		reflect.Uint16, reflect.Int16,
+		reflect.Uint32, reflect.Int32,
+		reflect.Uint64, reflect.Int64,
+		reflect.Float32, reflect.Float64,
+		reflect.String, reflect.Bool:
+		// These are expected base-cases. It may be extended when more field types are
+		// used in the config proto.
+		return
+	case reflect.Invalid:
+		// This is likely generated by filter_test.go.
+	default:
+		glog.Exitf("Unexpected type found in protobuf: %v. If this is expected, add another value to the case in filter.go", rc.Kind())
+	}
+}
diff --git a/config/config_parser/src/config_parser/filter_test.go b/config/config_parser/src/config_parser/filter_test.go
new file mode 100644
index 0000000..1abe3aa
--- /dev/null
+++ b/config/config_parser/src/config_parser/filter_test.go
@@ -0,0 +1,100 @@
+package config_parser
+
+import (
+	"config"
+	"reflect"
+	"testing"
+
+	"github.com/golang/protobuf/proto"
+)
+
+var filterTests = []struct {
+	before proto.Message
+	after  proto.Message
+}{
+	{
+		before: &config.MetricDefinition{
+			MetricName: "A Metric!",
+			MetaData: &config.MetricDefinition_Metadata{
+				ExpirationDate: "A DATE!",
+				Owner:          []string{"Someone"},
+			},
+		},
+		after: &config.MetricDefinition{
+			MetricName: "A Metric!",
+			MetaData:   &config.MetricDefinition_Metadata{},
+		},
+	},
+
+	{
+		before: &config.MetricDefinition{
+			MetricName: "A Metric!",
+			EventCodes: map[uint32]string{
+				0: "An event code",
+			},
+		},
+		after: &config.MetricDefinition{
+			MetricName: "A Metric!",
+		},
+	},
+
+	{
+		before: &config.ReportDefinition{
+			ReportName:    "A Report!",
+			CandidateList: []string{"Candidate1", "Candidate2", "Candidate3"},
+		},
+		after: &config.ReportDefinition{
+			ReportName: "A Report!",
+		},
+	},
+
+	{
+		before: &config.CobaltConfig{
+			Customers: []*config.CustomerConfig{
+				&config.CustomerConfig{
+					Projects: []*config.ProjectConfig{
+						&config.ProjectConfig{
+							Metrics: []*config.MetricDefinition{
+								&config.MetricDefinition{
+									MetricName: "A Metric!",
+									EventCodes: map[uint32]string{
+										0: "An event code",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		after: &config.CobaltConfig{
+			Customers: []*config.CustomerConfig{
+				&config.CustomerConfig{
+					Projects: []*config.ProjectConfig{
+						&config.ProjectConfig{
+							Metrics: []*config.MetricDefinition{
+								&config.MetricDefinition{
+									MetricName: "A Metric!",
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	},
+}
+
+func TestFilter(t *testing.T) {
+	for _, tt := range filterTests {
+		if proto.Equal(tt.before, tt.after) {
+			t.Errorf("%v\n==\n%v", proto.MarshalTextString(tt.before), proto.MarshalTextString(tt.after))
+		}
+
+		filterReflectedValue(reflect.ValueOf(tt.before))
+
+		if !proto.Equal(tt.before, tt.after) {
+			t.Errorf("%v\n!=\n%v", proto.MarshalTextString(tt.before), proto.MarshalTextString(tt.after))
+		}
+	}
+}
diff --git a/config/config_parser/src/config_parser_main.go b/config/config_parser/src/config_parser_main.go
index 5782e9e..fb31574 100644
--- a/config/config_parser/src/config_parser_main.go
+++ b/config/config_parser/src/config_parser_main.go
@@ -8,6 +8,7 @@
 package main
 
 import (
+	"config"
 	"config_parser"
 	"config_validator"
 	"flag"
@@ -20,6 +21,7 @@
 	"time"
 
 	"github.com/golang/glog"
+	"github.com/golang/protobuf/proto"
 )
 
 var (
@@ -40,6 +42,7 @@
 	varName        = flag.String("var_name", "config", "When using the 'cpp' or 'dart' output format, this will specify the variable name to be used in the output.")
 	namespace      = flag.String("namespace", "", "When using the 'cpp' or 'rust' output format, this will specify the comma-separated namespace within which the config variable must be places.")
 	depFile        = flag.String("dep_file", "", "Generate a depfile (see gn documentation) that lists all the project configuration files. Requires -output_file and -config_dir.")
+	forClient      = flag.Bool("for_client", false, "Filters out the hide_on_client tagged fields")
 
 	dartOutDir = flag.String("dart_out_dir", "", "The directory to write dart files to (if different from out_dir)")
 )
@@ -181,6 +184,11 @@
 	}
 
 	c := config_parser.MergeConfigs(configs)
+	filtered := proto.Clone(&c).(*config.CobaltConfig)
+
+	if *forClient {
+		config_parser.FilterHideOnClient(filtered)
+	}
 
 	for _, format := range outFormats {
 		var outputFormatter source_generator.OutputFormatter
@@ -214,7 +222,7 @@
 		}
 
 		// Then, we serialize the configuration.
-		configBytes, err := outputFormatter(&c)
+		configBytes, err := outputFormatter(&c, filtered)
 		if err != nil {
 			glog.Exit(err)
 		}
diff --git a/config/config_parser/src/source_generator/simple.go b/config/config_parser/src/source_generator/simple.go
index a3310d4..bd867a9 100644
--- a/config/config_parser/src/source_generator/simple.go
+++ b/config/config_parser/src/source_generator/simple.go
@@ -14,18 +14,18 @@
 )
 
 // Outputs the serialized proto.
-func BinaryOutput(c *config.CobaltConfig) (outputBytes []byte, err error) {
+func BinaryOutput(_, filtered *config.CobaltConfig) (outputBytes []byte, err error) {
 	buf := proto.Buffer{}
 	buf.SetDeterministic(true)
-	if err := buf.Marshal(c); err != nil {
+	if err := buf.Marshal(filtered); err != nil {
 		return nil, err
 	}
 	return buf.Bytes(), nil
 }
 
 // Outputs the serialized proto base64 encoded.
-func Base64Output(c *config.CobaltConfig) (outputBytes []byte, err error) {
-	configBytes, err := BinaryOutput(c)
+func Base64Output(c, filtered *config.CobaltConfig) (outputBytes []byte, err error) {
+	configBytes, err := BinaryOutput(c, filtered)
 	if err != nil {
 		return outputBytes, err
 	}
diff --git a/config/config_parser/src/source_generator/source_generator_test.go b/config/config_parser/src/source_generator/source_generator_test.go
index b54b55d..02f7e09 100644
--- a/config/config_parser/src/source_generator/source_generator_test.go
+++ b/config/config_parser/src/source_generator/source_generator_test.go
@@ -14,6 +14,7 @@
 	"strings"
 	"testing"
 
+	"github.com/golang/protobuf/proto"
 	"github.com/google/go-cmp/cmp"
 )
 
@@ -158,19 +159,34 @@
 	goldenFile     string
 	cobalt_version config_parser.CobaltVersion
 	formatter      OutputFormatter
+	hideOnClient   bool
 }{
-	{v0ProjectConfigYaml, "golden_v0.cb.h", config_parser.CobaltVersion0, CppOutputFactory("config", []string{"a", "b"})},
-	{v1ProjectConfigYaml, "golden_v1.cb.h", config_parser.CobaltVersion1, CppOutputFactory("config", []string{})},
-	{v0ProjectConfigYaml, "golden_v0.cb.dart", config_parser.CobaltVersion0, DartOutputFactory("config")},
-	{v1ProjectConfigYaml, "golden_v1.cb.dart", config_parser.CobaltVersion1, DartOutputFactory("config")},
-	{v0ProjectConfigYaml, "golden_v0.cb.rs", config_parser.CobaltVersion0, RustOutputFactory("config", []string{"a", "b"})},
-	{v1ProjectConfigYaml, "golden_v1.cb.rs", config_parser.CobaltVersion1, RustOutputFactory("config", []string{})},
+	{v0ProjectConfigYaml, "golden_v0.cb.dart", config_parser.CobaltVersion0, DartOutputFactory("config"), false},
+	{v0ProjectConfigYaml, "golden_v0.cb.h", config_parser.CobaltVersion0, CppOutputFactory("config", []string{"a", "b"}), false},
+	{v0ProjectConfigYaml, "golden_v0.cb.rs", config_parser.CobaltVersion0, RustOutputFactory("config", []string{"a", "b"}), false},
+
+	{v0ProjectConfigYaml, "golden_v0_filtered.cb.dart", config_parser.CobaltVersion0, DartOutputFactory("config"), true},
+	{v0ProjectConfigYaml, "golden_v0_filtered.cb.h", config_parser.CobaltVersion0, CppOutputFactory("config", []string{"a", "b"}), true},
+	{v0ProjectConfigYaml, "golden_v0_filtered.cb.rs", config_parser.CobaltVersion0, RustOutputFactory("config", []string{"a", "b"}), true},
+
+	{v1ProjectConfigYaml, "golden_v1.cb.dart", config_parser.CobaltVersion1, DartOutputFactory("config"), false},
+	{v1ProjectConfigYaml, "golden_v1.cb.h", config_parser.CobaltVersion1, CppOutputFactory("config", []string{}), false},
+	{v1ProjectConfigYaml, "golden_v1.cb.rs", config_parser.CobaltVersion1, RustOutputFactory("config", []string{}), false},
+
+	{v1ProjectConfigYaml, "golden_v1_filtered.cb.dart", config_parser.CobaltVersion1, DartOutputFactory("config"), true},
+	{v1ProjectConfigYaml, "golden_v1_filtered.cb.h", config_parser.CobaltVersion1, CppOutputFactory("config", []string{}), true},
+	{v1ProjectConfigYaml, "golden_v1_filtered.cb.rs", config_parser.CobaltVersion1, RustOutputFactory("config", []string{}), true},
 }
 
 func TestPrintConfig(t *testing.T) {
 	for _, tt := range cfgTests {
 		c := getConfigFrom(tt.yaml, tt.cobalt_version)
-		configBytes, err := tt.formatter(&c)
+		filtered := proto.Clone(&c).(*config.CobaltConfig)
+		if tt.hideOnClient {
+			config_parser.FilterHideOnClient(filtered)
+		}
+
+		configBytes, err := tt.formatter(&c, filtered)
 		if err != nil {
 			t.Errorf("Error generating file: %v", err)
 		}
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.dart b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.dart
new file mode 100644
index 0000000..f1ec8f3
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.dart
@@ -0,0 +1,20 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+// Metric ID Constants
+// Daily rare event counts
+// ignore: constant_identifier_names
+const int dailyRareEventCountsMetricId = 1;
+// Module views
+// ignore: constant_identifier_names
+const int moduleViewsMetricId = 2;
+
+// Report ID Constants
+// Fuchsia Ledger Daily Rare Events
+// ignore: constant_identifier_names
+const int fuchsiaLedgerDailyRareEventsReportId = 1;
+// Fuchsia Module Daily Launch Counts
+// ignore: constant_identifier_names
+const int fuchsiaModuleDailyLaunchCountsReportId = 2;
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+const String config = 'CmUIChAFGAEyXRUAAIA/OlYKDkxlZGdlci1zdGFydHVwCh1Db21taXRzLXJlY2VpdmVkLW91dC1vZi1vcmRlcgoOQ29tbWl0cy1tZXJnZWQKFU1lcmdlZC1jb21taXRzLW1lcmdlZAoMCAoQBRgCIgQIAhACEpgBCAoQBRgBIhdEYWlseSByYXJlIGV2ZW50IGNvdW50cypJRGFpbHkgY291bnRzIG9mIHNldmVyYWwgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIG9jY3VyIHJhcmVseSBpZiBldmVyLjIqCgpFdmVudCBuYW1lEhwKGldoaWNoIHJhcmUgZXZlbnQgb2NjdXJyZWQ/OAISfQgKEAUYAiIMTW9kdWxlIHZpZXdzKjVUcmFja3MgZWFjaCBpbmNpZGVuY2Ugb2Ygdmlld2luZyBhIG1vZHVsZSBieSBpdHMgVVJMLjIuCgN1cmwSJwolVGhlIFVSTCBvZiB0aGUgbW9kdWxlIGJlaW5nIGxhdW5jaGVkLjgCGqQBCAoQBRgBIiBGdWNoc2lhIExlZGdlciBEYWlseSBSYXJlIEV2ZW50cyo8QSBkYWlseSByZXBvcnQgb2YgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIGhhcHBlbiByYXJlbHkuMAFCDAoKRXZlbnQgbmFtZVICEANaKAoAEiQKImZ1Y2hzaWEtY29iYWx0LXJlcG9ydHMtcDItdGVzdC1hcHAaoAEIChAFGAIiIkZ1Y2hzaWEgTW9kdWxlIERhaWx5IExhdW5jaCBDb3VudHMqPUEgZGFpbHkgcmVwb3J0IG9mIHRoZSBkYWlseSBjb3VudHMgb2YgbW9kdWxlIGxhdW5jaGVzIGJ5IFVSTC4wAkIFCgN1cmxSAhADWigKABIkCiJmdWNoc2lhLWNvYmFsdC1yZXBvcnRzLXAyLXRlc3QtYXBw';
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.h b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.h
new file mode 100644
index 0000000..810b0bb
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.h
@@ -0,0 +1,22 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+#pragma once
+
+namespace a {
+namespace b {
+// Metric ID Constants
+// Daily rare event counts
+const uint32_t kDailyRareEventCountsMetricId = 1;
+// Module views
+const uint32_t kModuleViewsMetricId = 2;
+
+// Report ID Constants
+// Fuchsia Ledger Daily Rare Events
+const uint32_t kFuchsiaLedgerDailyRareEventsReportId = 1;
+// Fuchsia Module Daily Launch Counts
+const uint32_t kFuchsiaModuleDailyLaunchCountsReportId = 2;
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+const char kConfig[] = "CmUIChAFGAEyXRUAAIA/OlYKDkxlZGdlci1zdGFydHVwCh1Db21taXRzLXJlY2VpdmVkLW91dC1vZi1vcmRlcgoOQ29tbWl0cy1tZXJnZWQKFU1lcmdlZC1jb21taXRzLW1lcmdlZAoMCAoQBRgCIgQIAhACEpgBCAoQBRgBIhdEYWlseSByYXJlIGV2ZW50IGNvdW50cypJRGFpbHkgY291bnRzIG9mIHNldmVyYWwgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIG9jY3VyIHJhcmVseSBpZiBldmVyLjIqCgpFdmVudCBuYW1lEhwKGldoaWNoIHJhcmUgZXZlbnQgb2NjdXJyZWQ/OAISfQgKEAUYAiIMTW9kdWxlIHZpZXdzKjVUcmFja3MgZWFjaCBpbmNpZGVuY2Ugb2Ygdmlld2luZyBhIG1vZHVsZSBieSBpdHMgVVJMLjIuCgN1cmwSJwolVGhlIFVSTCBvZiB0aGUgbW9kdWxlIGJlaW5nIGxhdW5jaGVkLjgCGqQBCAoQBRgBIiBGdWNoc2lhIExlZGdlciBEYWlseSBSYXJlIEV2ZW50cyo8QSBkYWlseSByZXBvcnQgb2YgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIGhhcHBlbiByYXJlbHkuMAFCDAoKRXZlbnQgbmFtZVICEANaKAoAEiQKImZ1Y2hzaWEtY29iYWx0LXJlcG9ydHMtcDItdGVzdC1hcHAaoAEIChAFGAIiIkZ1Y2hzaWEgTW9kdWxlIERhaWx5IExhdW5jaCBDb3VudHMqPUEgZGFpbHkgcmVwb3J0IG9mIHRoZSBkYWlseSBjb3VudHMgb2YgbW9kdWxlIGxhdW5jaGVzIGJ5IFVSTC4wAkIFCgN1cmxSAhADWigKABIkCiJmdWNoc2lhLWNvYmFsdC1yZXBvcnRzLXAyLXRlc3QtYXBw";
+}
+}
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.rs b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.rs
new file mode 100644
index 0000000..123857f
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v0_filtered.cb.rs
@@ -0,0 +1,20 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+pub mod a {
+pub mod b {
+// Metric ID Constants
+// Daily rare event counts
+pub const DAILY_RARE_EVENT_COUNTS_METRIC_ID = 1;
+// Module views
+pub const MODULE_VIEWS_METRIC_ID = 2;
+
+// Report ID Constants
+// Fuchsia Ledger Daily Rare Events
+pub const FUCHSIA_LEDGER_DAILY_RARE_EVENTS_REPORT_ID = 1;
+// Fuchsia Module Daily Launch Counts
+pub const FUCHSIA_MODULE_DAILY_LAUNCH_COUNTS_REPORT_ID = 2;
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+pub const CONFIG: &str = "CmUIChAFGAEyXRUAAIA/OlYKDkxlZGdlci1zdGFydHVwCh1Db21taXRzLXJlY2VpdmVkLW91dC1vZi1vcmRlcgoOQ29tbWl0cy1tZXJnZWQKFU1lcmdlZC1jb21taXRzLW1lcmdlZAoMCAoQBRgCIgQIAhACEpgBCAoQBRgBIhdEYWlseSByYXJlIGV2ZW50IGNvdW50cypJRGFpbHkgY291bnRzIG9mIHNldmVyYWwgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIG9jY3VyIHJhcmVseSBpZiBldmVyLjIqCgpFdmVudCBuYW1lEhwKGldoaWNoIHJhcmUgZXZlbnQgb2NjdXJyZWQ/OAISfQgKEAUYAiIMTW9kdWxlIHZpZXdzKjVUcmFja3MgZWFjaCBpbmNpZGVuY2Ugb2Ygdmlld2luZyBhIG1vZHVsZSBieSBpdHMgVVJMLjIuCgN1cmwSJwolVGhlIFVSTCBvZiB0aGUgbW9kdWxlIGJlaW5nIGxhdW5jaGVkLjgCGqQBCAoQBRgBIiBGdWNoc2lhIExlZGdlciBEYWlseSBSYXJlIEV2ZW50cyo8QSBkYWlseSByZXBvcnQgb2YgZXZlbnRzIHRoYXQgYXJlIGV4cGVjdGVkIHRvIGhhcHBlbiByYXJlbHkuMAFCDAoKRXZlbnQgbmFtZVICEANaKAoAEiQKImZ1Y2hzaWEtY29iYWx0LXJlcG9ydHMtcDItdGVzdC1hcHAaoAEIChAFGAIiIkZ1Y2hzaWEgTW9kdWxlIERhaWx5IExhdW5jaCBDb3VudHMqPUEgZGFpbHkgcmVwb3J0IG9mIHRoZSBkYWlseSBjb3VudHMgb2YgbW9kdWxlIGxhdW5jaGVzIGJ5IFVSTC4wAkIFCgN1cmxSAhADWigKABIkCiJmdWNoc2lhLWNvYmFsdC1yZXBvcnRzLXAyLXRlc3QtYXBw";
+}
+}
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.dart b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.dart
new file mode 100644
index 0000000..6d6dc82
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.dart
@@ -0,0 +1,22 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+// Metric ID Constants
+// the_metric_name
+// ignore: constant_identifier_names
+const int theMetricNameMetricId = 238615413;
+// the_other_metric_name
+// ignore: constant_identifier_names
+const int theOtherMetricNameMetricId = 2739731282;
+
+// Enum for the_other_metric_name (EventCode)
+class TheOtherMetricNameEventCode {
+  static const int AnEvent = 0;
+  static const int AnotherEvent = 1;
+  static const int AThirdEvent = 2;
+}
+const int TheOtherMetricNameEventCode_AnEvent = TheOtherMetricNameEventCode::AnEvent;
+const int TheOtherMetricNameEventCode_AnotherEvent = TheOtherMetricNameEventCode::AnotherEvent;
+const int TheOtherMetricNameEventCode_AThirdEvent = TheOtherMetricNameEventCode::AThirdEvent;
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+const String config = 'KqkBCghjdXN0b21lchAKGpoBCgdwcm9qZWN0EAUaTQoPdGhlX21ldHJpY19uYW1lEAoYBSD19uNxYhUKCnRoZV9yZXBvcnQQu6WL8QgYj05iGgoQdGhlX290aGVyX3JlcG9ydBDK3M3qARgGGj4KFXRoZV9vdGhlcl9tZXRyaWNfbmFtZRAKGAUg0vazmgooATjIAVABYhQKCnRoZV9yZXBvcnQQu6WL8QgYBw==';
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.h b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.h
new file mode 100644
index 0000000..9eede13
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.h
@@ -0,0 +1,22 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+#pragma once
+
+// Metric ID Constants
+// the_metric_name
+const uint32_t kTheMetricNameMetricId = 238615413;
+// the_other_metric_name
+const uint32_t kTheOtherMetricNameMetricId = 2739731282;
+
+// Enum for the_other_metric_name (EventCode)
+enum class TheOtherMetricNameEventCode {
+  AnEvent = 0,
+  AnotherEvent = 1,
+  AThirdEvent = 2,
+};
+const TheOtherMetricNameEventCode TheOtherMetricNameEventCode_AnEvent = TheOtherMetricNameEventCode::AnEvent;
+const TheOtherMetricNameEventCode TheOtherMetricNameEventCode_AnotherEvent = TheOtherMetricNameEventCode::AnotherEvent;
+const TheOtherMetricNameEventCode TheOtherMetricNameEventCode_AThirdEvent = TheOtherMetricNameEventCode::AThirdEvent;
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+const char kConfig[] = "KqkBCghjdXN0b21lchAKGpoBCgdwcm9qZWN0EAUaTQoPdGhlX21ldHJpY19uYW1lEAoYBSD19uNxYhUKCnRoZV9yZXBvcnQQu6WL8QgYj05iGgoQdGhlX290aGVyX3JlcG9ydBDK3M3qARgGGj4KFXRoZV9vdGhlcl9tZXRyaWNfbmFtZRAKGAUg0vazmgooATjIAVABYhQKCnRoZV9yZXBvcnQQu6WL8QgYBw==";
diff --git a/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.rs b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.rs
new file mode 100644
index 0000000..e7f40e1
--- /dev/null
+++ b/config/config_parser/src/source_generator/source_generator_test_files/golden_v1_filtered.cb.rs
@@ -0,0 +1,17 @@
+// This file was generated by Cobalt's Config parser based on the configuration
+// YAML in the cobalt_config repository. Edit the YAML there to make changes.
+// Metric ID Constants
+// the_metric_name
+pub const THE_METRIC_NAME_METRIC_ID = 238615413;
+// the_other_metric_name
+pub const THE_OTHER_METRIC_NAME_METRIC_ID = 2739731282;
+
+// Enum for the_other_metric_name (EventCode)
+pub enum TheOtherMetricNameEventCode {
+  AnEvent = 0,
+  AnotherEvent = 1,
+  AThirdEvent = 2,
+}
+
+// The base64 encoding of the bytes of a serialized CobaltConfig proto message.
+pub const CONFIG: &str = "KqkBCghjdXN0b21lchAKGpoBCgdwcm9qZWN0EAUaTQoPdGhlX21ldHJpY19uYW1lEAoYBSD19uNxYhUKCnRoZV9yZXBvcnQQu6WL8QgYj05iGgoQdGhlX290aGVyX3JlcG9ydBDK3M3qARgGGj4KFXRoZV9vdGhlcl9tZXRyaWNfbmFtZRAKGAUg0vazmgooATjIAVABYhQKCnRoZV9yZXBvcnQQu6WL8QgYBw==";
diff --git a/config/config_parser/src/source_generator/source_outputter.go b/config/config_parser/src/source_generator/source_outputter.go
index 54e8712..5a0701c 100644
--- a/config/config_parser/src/source_generator/source_outputter.go
+++ b/config/config_parser/src/source_generator/source_outputter.go
@@ -28,7 +28,7 @@
 	writeStringConstant(so *sourceOutputter, value string, name ...string)
 }
 
-type OutputFormatter func(c *config.CobaltConfig) (outputBytes []byte, err error)
+type OutputFormatter func(c, filtered *config.CobaltConfig) (outputBytes []byte, err error)
 
 type sourceOutputter struct {
 	buffer     *bytes.Buffer
@@ -186,7 +186,7 @@
 	return nil
 }
 
-func (so *sourceOutputter) writeFile(c *config.CobaltConfig) error {
+func (so *sourceOutputter) writeFile(c, filtered *config.CobaltConfig) error {
 	so.writeGenerationWarning()
 
 	so.language.writeExtraHeader(so)
@@ -201,7 +201,7 @@
 		so.writeLegacyConstants(c)
 	}
 
-	b64Bytes, err := Base64Output(c)
+	b64Bytes, err := Base64Output(c, filtered)
 	if err != nil {
 		return err
 	}
@@ -217,8 +217,8 @@
 }
 
 func (so *sourceOutputter) getOutputFormatter() OutputFormatter {
-	return func(c *config.CobaltConfig) (outputBytes []byte, err error) {
-		err = so.writeFile(c)
+	return func(c, filtered *config.CobaltConfig) (outputBytes []byte, err error) {
+		err = so.writeFile(c, filtered)
 		return so.Bytes(), err
 	}
 }
diff --git a/config/metric_definition.proto b/config/metric_definition.proto
index 52a51b7..9419ded 100644
--- a/config/metric_definition.proto
+++ b/config/metric_definition.proto
@@ -7,6 +7,7 @@
 
 import "config/metrics.proto";
 import "config/report_definition.proto";
+import "config/annotations.proto";
 
 option go_package = "config";
 
@@ -187,12 +188,11 @@
   //
   // This field is used in most Metric types.
   //
-  // The keys are the numeric codes and the values are the human-readable
-  // labels for the codes. It is OK to add new elements to this map or to change
-  // the spelling of labels after data collection has started. It is not OK to
+  // The keys are the numeric codes and the values are the human-readable labels
+  // for the codes. It is OK to add new elements to this map or to change the
+  // spelling of labels after data collection has started. It is not OK to
   // change the meaning of any of the codes.
-  // ##DO_NOT_POPULATE_ON_CLIENT##
-  map<uint32, string> event_codes = 6;
+  map<uint32, string> event_codes = 6 [(cobalt_options).hide_on_client = true];
 
   // Only needed with Metrics of type EVENT_OCCURRED.
   // While additional event_codes may be added after data collection has begun,
@@ -232,12 +232,12 @@
     //
     // The date must be expressed in yyyy/mm/dd form.
     // It may be at most one year in the future.
-    string expiration_date = 1;
+    string expiration_date = 1 [(cobalt_options).hide_on_client = true];
 
     // Primary contacts for questions/bugs regarding this metric (may be a
     // group). This should be a fully qualified email address (e.g.
     // my-group@test.com)
-    repeated string owner = 2;
+    repeated string owner = 2 [(cobalt_options).hide_on_client = true];
 
     // Maximum ReleaseStage for which this Metric is allowed to be collected.
     ReleaseStage max_release_stage = 4;
diff --git a/config/report_definition.proto b/config/report_definition.proto
index a33e2fe..5ef43cf 100644
--- a/config/report_definition.proto
+++ b/config/report_definition.proto
@@ -8,6 +8,7 @@
 import "config/metrics.proto";
 import "config/report_configs.proto";
 import "config/window_size.proto";
+import "config/annotations.proto";
 
 option go_package = "config";
 
@@ -416,8 +417,7 @@
   // should be used, not both. Used for the hashed |component_name|
   // field for several Report types and the encoded |string| field for the
   // HIGH_FREQUENCY_STRING_COUNTS Report type.
-  // ##DO_NOT_POPULATE_ON_CLIENT##
-  repeated string candidate_list = 8;
+  repeated string candidate_list = 8 [(cobalt_options).hide_on_client = true];
 
   // Simple name or full path to file containing known string values.
   // Either this or |candidate_list| should be used, not both. Used for the