Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/gcpspanner/browser_feature_support_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func calculateBrowserSupportEvents(
// PrecalculateBrowserFeatureSupportEvents populates the BrowserFeatureSupportEvents table with pre-calculated data.
func (c *Client) PrecalculateBrowserFeatureSupportEvents(ctx context.Context, startAt, endAt time.Time) error {
txn := c.ReadOnlyTransaction()
defer txn.Close()

// 1. Fetch all BrowserFeatureAvailabilities
availabilities, err := c.fetchAllBrowserAvailabilitiesWithTransaction(ctx, txn)
if err != nil {
Expand Down
66 changes: 46 additions & 20 deletions lib/gcpspanner/spanneradapters/chromium_historgram_enum_consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import (
"errors"
"log/slog"
"strings"
"unicode"

"github.com/GoogleChrome/webstatus.dev/lib/gcpspanner"
"github.com/GoogleChrome/webstatus.dev/lib/metricdatatypes"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)

// ChromiumHistogramEnumConsumer handles the conversion of histogram between the workflow/API input
Expand All @@ -43,10 +44,20 @@ type ChromiumHistogramEnumsClient interface {
UpsertChromiumHistogramEnumValue(context.Context, gcpspanner.ChromiumHistogramEnumValue) (*string, error)
UpsertWebFeatureChromiumHistogramEnumValue(context.Context, gcpspanner.WebFeatureChromiumHistogramEnumValue) error
GetIDFromFeatureKey(context.Context, *gcpspanner.FeatureIDFilter) (*string, error)
FetchAllFeatureKeys(context.Context) ([]string, error)
}

// Used by GCP Log-based metrics to extract the data about mismatch mappings.
const logMissingFeatureIDMetricMsg = "unable to find feature ID. skipping mapping"

func (c *ChromiumHistogramEnumConsumer) SaveHistogramEnums(
ctx context.Context, data metricdatatypes.HistogramMapping) error {
featureKeys, err := c.client.FetchAllFeatureKeys(ctx)
if err != nil {
return errors.Join(ErrFailedToGetFeatureKeys, err)
}
enumToFeatureKeyMap := createEnumToFeatureKeyMap(featureKeys)
// Create mapping of anticipated enums to feature keys
for histogram, enums := range data {
enumID, err := c.client.UpsertChromiumHistogramEnum(ctx, gcpspanner.ChromiumHistogramEnum{
HistogramName: string(histogram),
Expand All @@ -63,12 +74,21 @@ func (c *ChromiumHistogramEnumConsumer) SaveHistogramEnums(
if err != nil {
return errors.Join(ErrFailedToStoreEnumValue, err)
}
featureKey := enumLabelToFeatureKey(enum.Label)

featureKey, found := enumToFeatureKeyMap[enum.Label]
if !found {
slog.WarnContext(ctx,
logMissingFeatureIDMetricMsg,
"label", enum.Label)

continue
}

featureID, err := c.client.GetIDFromFeatureKey(
ctx, gcpspanner.NewFeatureKeyFilter(featureKey))
if err != nil {
slog.WarnContext(ctx,
"unable to find feature ID. skipping mapping",
logMissingFeatureIDMetricMsg,
"error", err,
"featureKey", featureKey,
"label", enum.Label)
Expand Down Expand Up @@ -97,29 +117,35 @@ var (
// the mapping between enum value and web feature.
ErrFailedToStoreEnumValueWebFeatureMapping = errors.New(
"failed to store web feature to chromium enum value mapping")
// ErrFailedToGetFeatureKeys indicates an internal error when trying to get all the feature keys.
ErrFailedToGetFeatureKeys = errors.New("failed to get feature keys")
)

func enumLabelToFeatureKey(label string) string {
b := strings.Builder{}
for idx, c := range label {
// First character just return the lower case version of it.
if idx == 0 {
b.WriteRune(unicode.ToLower(c))
// nolint:lll // WONTFIX: useful comment message
// createEnumToFeatureKeyMap uses the list of WebDX feature keys to
// generate a map from the enum label (e.g., "ViewTransitions")
// back to its original WebDX feature key (e.g., "view-transitions").
// It uses the same transformation logic described in the Chromium mojom file.
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom;l=35-47;drc=822a70f9ac61a75babe9d24ddfc32ab475acc7e1
func createEnumToFeatureKeyMap(featureKeys []string) map[string]string {
titleCaser := cases.Title(language.English)
m := make(map[string]string, len(featureKeys))
specialCases := map[string]string{
"float16array": "Float16Array",
"uint8array-base64-hex": "Uint8ArrayBase64Hex",
}
for _, featureKey := range featureKeys {
if specialCaseLabel, found := specialCases[featureKey]; found {
m[specialCaseLabel] = featureKey

continue
}
if unicode.IsUpper(c) {
b.WriteRune('-')
b.WriteRune(unicode.ToLower(c))

continue
}
// Add hyphen if previous character is a letter and current character is a digit
if idx > 0 && unicode.IsLetter(rune(label[idx-1])) && unicode.IsDigit(c) {
b.WriteRune('-')
}
b.WriteRune(c)
enumLabel := titleCaser.String(featureKey)
enumLabel = strings.ReplaceAll(enumLabel, "-", "")
// Before storing it, check if it exists
m[enumLabel] = featureKey
}

return b.String()
return m
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package spanneradapters
import (
"context"
"errors"
"reflect"
"testing"

"github.com/GoogleChrome/webstatus.dev/lib/gcpspanner"
Expand All @@ -31,6 +32,7 @@ type mockChromiumHistogramEnumsClient struct {
upsertWebFeatureChromiumHistogramEnumValue func(context.Context,
gcpspanner.WebFeatureChromiumHistogramEnumValue) error
getIDFromFeatureKey func(context.Context, *gcpspanner.FeatureIDFilter) (*string, error)
fetchAllFeatureKeys func(context.Context) ([]string, error)
}

func (m *mockChromiumHistogramEnumsClient) UpsertChromiumHistogramEnum(ctx context.Context,
Expand All @@ -53,6 +55,11 @@ func (m *mockChromiumHistogramEnumsClient) GetIDFromFeatureKey(ctx context.Conte
return m.getIDFromFeatureKey(ctx, in)
}

func (m *mockChromiumHistogramEnumsClient) FetchAllFeatureKeys(
ctx context.Context) ([]string, error) {
return m.fetchAllFeatureKeys(ctx)
}

func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
tests := []struct {
name string
Expand All @@ -63,6 +70,9 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
{
name: "Success",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return []string{"enum-label"}, nil
},
upsertChromiumHistogramEnum: func(_ context.Context,
_ gcpspanner.ChromiumHistogramEnum) (*string, error) {
return valuePtr("enumID"), nil
Expand All @@ -87,9 +97,30 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
},
expectedErr: nil,
},
{
name: "FetchAllFeatureKeys returns error",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return nil, errors.New("test error")
},
upsertChromiumHistogramEnum: nil,
upsertChromiumHistogramEnumValue: nil,
upsertWebFeatureChromiumHistogramEnumValue: nil,
getIDFromFeatureKey: nil,
},
data: metricdatatypes.HistogramMapping{
metricdatatypes.WebDXFeatureEnum: []metricdatatypes.HistogramEnumValue{
{Value: 1, Label: "EnumLabel"},
},
},
expectedErr: ErrFailedToGetFeatureKeys,
},
{
name: "UpsertChromiumHistogramEnum returns error",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return []string{"enum-label"}, nil
},
upsertChromiumHistogramEnum: func(_ context.Context,
_ gcpspanner.ChromiumHistogramEnum) (*string, error) {
return nil, errors.New("test error")
Expand All @@ -108,6 +139,9 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
{
name: "UpsertChromiumHistogramEnumValue returns error",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return []string{"enum-label"}, nil
},
upsertChromiumHistogramEnum: func(_ context.Context,
_ gcpspanner.ChromiumHistogramEnum) (*string, error) {
return valuePtr("enumID"), nil
Expand All @@ -129,6 +163,9 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
{
name: "GetIDFromFeatureKey returns error",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return []string{"enum-label"}, nil
},
upsertChromiumHistogramEnum: func(_ context.Context,
_ gcpspanner.ChromiumHistogramEnum) (*string, error) {
return valuePtr("enumID"), nil
Expand All @@ -153,6 +190,9 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
{
name: "UpsertWebFeatureChromiumHistogramEnumValue returns error",
client: &mockChromiumHistogramEnumsClient{
fetchAllFeatureKeys: func(_ context.Context) ([]string, error) {
return []string{"enum-label"}, nil
},
upsertChromiumHistogramEnum: func(_ context.Context,
_ gcpspanner.ChromiumHistogramEnum) (*string, error) {
return valuePtr("enumID"), nil
Expand Down Expand Up @@ -195,48 +235,34 @@ func TestChromiumHistogramEnumConsumer_SaveHistogramEnums(t *testing.T) {
}
}

func TestEnumLabelToFeatureKey(t *testing.T) {
tests := []struct {
name string
label string
want string
}{
{
name: "Simple lowercase",
label: "simple",
want: "simple",
},
{
name: "typical",
label: "TypicalCase",
want: "typical-case",
},
{
name: "With numbers in the middle",
label: "With123Numbers",
want: "with-123-numbers",
},
{
name: "Starting with number",
label: "123Abc",
want: "123-abc",
},
{
name: "Consecutive uppercase letters",
label: "ABCTest",
want: "a-b-c-test",
},
{
name: "Mixed case with numbers and consecutive uppercase",
label: "ABC123defGHI456Jkl",
want: "a-b-c-123def-g-h-i-456-jkl",
},
func TestCreateEnumToFeatureKeyMap(t *testing.T) {
featureKeys := []string{
"canvas-2d-color-management",
"http3",
"intersection-observer-v2",
"view-transitions",
// Special cases
"float16array",
"uint8array-base64-hex",
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := enumLabelToFeatureKey(tc.label); got != tc.want {
t.Errorf("enumLabelToFeatureKey() = %v, want %v", got, tc.want)
}
})
// nolint: lll // WONTFIX: useful comment with SHA
want := map[string]string{
"Canvas2DColorManagement": "canvas-2d-color-management",
"Http3": "http3",
"IntersectionObserverV2": "intersection-observer-v2",
"ViewTransitions": "view-transitions",
/*
Special cases
*/
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom;l=360;drc=822a70f9ac61a75babe9d24ddfc32ab475acc7e1
// https://github.com/web-platform-dx/web-features/blob/main/features/float16array.yml
"Float16Array": "float16array",
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/public/mojom/use_counter/metrics/webdx_feature.mojom;l=396;drc=822a70f9ac61a75babe9d24ddfc32ab475acc7e1
// https://github.com/web-platform-dx/web-features/blob/main/features/uint8array-base64-hex.yml
"Uint8ArrayBase64Hex": "uint8array-base64-hex",
}
got := createEnumToFeatureKeyMap(featureKeys)
if !reflect.DeepEqual(got, want) {
t.Errorf("createEnumToFeatureKeyMap()\ngot: (%+v)\nwant: (%+v)\n", got, want)
}
}
24 changes: 18 additions & 6 deletions lib/gcpspanner/web_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,21 +102,33 @@ func (c *Client) GetIDFromFeatureKey(ctx context.Context, filter *FeatureIDFilte

func (c *Client) fetchAllWebFeatureIDsWithTransaction(
ctx context.Context, txn *spanner.ReadOnlyTransaction) ([]string, error) {
var ids []string
iter := txn.Read(ctx, webFeaturesTable, spanner.AllKeys(), []string{"ID"})
return fetchColumnValuesWithTransaction[string](ctx, txn, webFeaturesTable, "ID")
}

func (c *Client) FetchAllFeatureKeys(ctx context.Context) ([]string, error) {
txn := c.ReadOnlyTransaction()
defer txn.Close()

return fetchColumnValuesWithTransaction[string](ctx, txn, webFeaturesTable, "FeatureKey")
}

func fetchColumnValuesWithTransaction[T any](
ctx context.Context, txn *spanner.ReadOnlyTransaction, table string, columnName string) ([]T, error) {
var values []T
iter := txn.Read(ctx, table, spanner.AllKeys(), []string{columnName})
defer iter.Stop()
err := iter.Do(func(row *spanner.Row) error {
var id string
if err := row.Column(0, &id); err != nil {
var value T
if err := row.Column(0, &value); err != nil {
return err
}
ids = append(ids, id)
values = append(values, value)

return nil
})
if err != nil {
return nil, err
}

return ids, nil
return values, nil
}
15 changes: 15 additions & 0 deletions lib/gcpspanner/web_features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,19 @@ func TestUpsertWebFeature(t *testing.T) {
if !slices.Equal[[]WebFeature](expectedPageAfterUpdate, features) {
t.Errorf("unequal features after update. expected %+v actual %+v", sampleFeatures, features)
}

expectedKeys := []string{
"feature1",
"feature2",
"feature3",
"feature4",
}
keys, err := spannerClient.FetchAllFeatureKeys(ctx)
if err != nil {
t.Errorf("unexpected error fetching all keys")
}
slices.Sort(keys)
if !slices.Equal(keys, expectedKeys) {
t.Errorf("unequal keys. expected %+v actual %+v", expectedKeys, keys)
}
}
2 changes: 1 addition & 1 deletion lib/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ require (
golang.org/x/oauth2 v0.29.0
golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
golang.org/x/text v0.24.0
golang.org/x/time v0.11.0 // indirect
google.golang.org/genproto v0.0.0-20250428153025-10db94c68c34 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect
Expand Down
Loading