Skip to content

Commit 4fdd236

Browse files
Populate feature flag telemetry metadata (#47)
* populate feature flag telemetry metadata * update
1 parent 04a15f0 commit 4fdd236

2 files changed

Lines changed: 301 additions & 0 deletions

File tree

azureappconfiguration/azureappconfiguration.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,14 +478,22 @@ func (azappcfg *AzureAppConfiguration) loadFeatureFlags(ctx context.Context, set
478478
return err
479479
}
480480

481+
// endpoint used in feature flag reference
482+
var endpoint string
481483
dedupFeatureFlags := make(map[string]any, len(settingsResponse.settings))
484+
if manager, ok := azappcfg.clientManager.(*configurationClientManager); ok {
485+
endpoint = manager.endpoint
486+
}
487+
482488
for _, setting := range settingsResponse.settings {
483489
if setting.Key != nil {
484490
var v map[string]any
485491
if err := json.Unmarshal([]byte(*setting.Value), &v); err != nil {
486492
log.Printf("Invalid feature flag setting: key=%s, error=%s, just ignore", *setting.Key, err.Error())
487493
continue
488494
}
495+
496+
populateTelemetryMetadata(v, setting, endpoint)
489497
azappcfg.updateFeatureFlagTracing(v)
490498
dedupFeatureFlags[*setting.Key] = v
491499
}
@@ -956,3 +964,32 @@ func isFailoverable(err error) bool {
956964

957965
return false
958966
}
967+
968+
func generateFeatureFlagReference(setting azappconfig.Setting, endpoint string) string {
969+
featureFlagReference := fmt.Sprintf("%s/kv/%s", endpoint, *setting.Key)
970+
971+
// Check if the label is present and not empty
972+
if setting.Label != nil && strings.TrimSpace(*setting.Label) != "" {
973+
featureFlagReference += fmt.Sprintf("?label=%s", *setting.Label)
974+
}
975+
976+
return featureFlagReference
977+
}
978+
979+
func populateTelemetryMetadata(featureFlag map[string]any, setting azappconfig.Setting, endpoint string) {
980+
if telemetry, ok := featureFlag[telemetryKey].(map[string]any); ok {
981+
if enabled, ok := telemetry[enabledKey].(bool); ok && enabled {
982+
metadata, _ := telemetry[metadataKey].(map[string]any)
983+
if metadata == nil {
984+
metadata = make(map[string]any)
985+
}
986+
987+
// Set the new metadata
988+
if setting.ETag != nil {
989+
metadata[eTagKey] = *setting.ETag
990+
}
991+
metadata[featureFlagReferenceKey] = generateFeatureFlagReference(setting, endpoint)
992+
telemetry[metadataKey] = metadata
993+
}
994+
}
995+
}

azureappconfiguration/azureappconfiguration_test.go

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1601,3 +1601,267 @@ func TestLoadFeatureFlags_TracingUpdated(t *testing.T) {
16011601
// Verify max variants is included
16021602
assert.Contains(t, correlationCtx, tracing.FFMaxVariantsKey+"=3")
16031603
}
1604+
1605+
func TestLoadFeatureFlags_WithTelemetryEnabled(t *testing.T) {
1606+
ctx := context.Background()
1607+
mockClient := new(mockSettingsClient)
1608+
1609+
// Feature flag with telemetry enabled
1610+
value1 := `{
1611+
"id": "TelemetryFlag",
1612+
"description": "Feature flag with telemetry",
1613+
"enabled": true,
1614+
"telemetry": {
1615+
"enabled": true
1616+
}
1617+
}`
1618+
1619+
// Feature flag with telemetry disabled
1620+
value2 := `{
1621+
"id": "NoTelemetryFlag",
1622+
"description": "Feature flag without telemetry",
1623+
"enabled": false,
1624+
"telemetry": {
1625+
"enabled": false
1626+
}
1627+
}`
1628+
1629+
// Feature flag without telemetry section
1630+
value3 := `{
1631+
"id": "BasicFlag",
1632+
"description": "Basic feature flag",
1633+
"enabled": true
1634+
}`
1635+
1636+
eTag1 := "W/\"etag-1\""
1637+
eTag2 := "W/\"etag-2\""
1638+
eTag3 := "W/\"etag-3\""
1639+
1640+
mockResponse := &settingsResponse{
1641+
settings: []azappconfig.Setting{
1642+
{
1643+
Key: toPtr(".appconfig.featureflag/TelemetryFlag"),
1644+
Value: &value1,
1645+
ContentType: toPtr(featureFlagContentType),
1646+
ETag: (*azcore.ETag)(&eTag1),
1647+
Label: toPtr("production"),
1648+
},
1649+
{
1650+
Key: toPtr(".appconfig.featureflag/NoTelemetryFlag"),
1651+
Value: &value2,
1652+
ContentType: toPtr(featureFlagContentType),
1653+
ETag: (*azcore.ETag)(&eTag2),
1654+
},
1655+
{
1656+
Key: toPtr(".appconfig.featureflag/BasicFlag"),
1657+
Value: &value3,
1658+
ContentType: toPtr(featureFlagContentType),
1659+
ETag: (*azcore.ETag)(&eTag3),
1660+
},
1661+
},
1662+
pageETags: map[Selector][]*azcore.ETag{},
1663+
}
1664+
1665+
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
1666+
1667+
azappcfg := &AzureAppConfiguration{
1668+
clientManager: &configurationClientManager{
1669+
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
1670+
endpoint: "https://mystore.azconfig.io",
1671+
},
1672+
ffSelectors: getFeatureFlagSelectors([]Selector{}),
1673+
featureFlags: make(map[string]any),
1674+
tracingOptions: tracing.Options{
1675+
Enabled: true,
1676+
FeatureFlagTracing: &tracing.FeatureFlagTracing{},
1677+
},
1678+
}
1679+
1680+
err := azappcfg.loadFeatureFlags(ctx, mockClient)
1681+
assert.NoError(t, err)
1682+
1683+
// Verify feature flags structure
1684+
featureManagement := azappcfg.featureFlags[featureManagementSectionKey].(map[string]any)
1685+
featureFlags := featureManagement[featureFlagSectionKey].([]any)
1686+
assert.Len(t, featureFlags, 3)
1687+
1688+
// Find and verify TelemetryFlag
1689+
var telemetryFlag map[string]any
1690+
var noTelemetryFlag map[string]any
1691+
var basicFlag map[string]any
1692+
1693+
for _, flag := range featureFlags {
1694+
flagMap := flag.(map[string]any)
1695+
switch flagMap["id"] {
1696+
case "TelemetryFlag":
1697+
telemetryFlag = flagMap
1698+
case "NoTelemetryFlag":
1699+
noTelemetryFlag = flagMap
1700+
case "BasicFlag":
1701+
basicFlag = flagMap
1702+
}
1703+
}
1704+
1705+
// Verify TelemetryFlag has telemetry metadata populated
1706+
assert.NotNil(t, telemetryFlag, "TelemetryFlag should exist")
1707+
telemetry, ok := telemetryFlag[telemetryKey].(map[string]any)
1708+
assert.True(t, ok, "TelemetryFlag should have telemetry section")
1709+
assert.True(t, telemetry[enabledKey].(bool), "Telemetry should be enabled")
1710+
1711+
metadata, ok := telemetry[metadataKey].(map[string]any)
1712+
assert.True(t, ok, "Telemetry should have metadata")
1713+
assert.Equal(t, "https://mystore.azconfig.io/kv/.appconfig.featureflag/TelemetryFlag?label=production",
1714+
metadata[featureFlagReferenceKey], "Feature flag reference should be populated with label")
1715+
1716+
// Verify NoTelemetryFlag does not have metadata populated
1717+
assert.NotNil(t, noTelemetryFlag, "NoTelemetryFlag should exist")
1718+
noTelemetry, ok := noTelemetryFlag[telemetryKey].(map[string]any)
1719+
assert.True(t, ok, "NoTelemetryFlag should have telemetry section")
1720+
assert.False(t, noTelemetry[enabledKey].(bool), "Telemetry should be disabled")
1721+
1722+
// Should not have metadata since telemetry is disabled
1723+
_, hasMetadata := noTelemetry[metadataKey]
1724+
assert.False(t, hasMetadata, "NoTelemetryFlag should not have metadata when telemetry is disabled")
1725+
1726+
// Verify BasicFlag does not have telemetry section modified
1727+
assert.NotNil(t, basicFlag, "BasicFlag should exist")
1728+
_, hasTelemetry := basicFlag[telemetryKey]
1729+
assert.False(t, hasTelemetry, "BasicFlag should not have telemetry section")
1730+
1731+
// Verify tracing was updated
1732+
assert.True(t, azappcfg.tracingOptions.FeatureFlagTracing.UsesTelemetry, "Should detect telemetry usage")
1733+
}
1734+
1735+
func TestLoadFeatureFlags_TelemetryWithExistingMetadata(t *testing.T) {
1736+
ctx := context.Background()
1737+
mockClient := new(mockSettingsClient)
1738+
1739+
// Feature flag with telemetry enabled and existing metadata
1740+
value1 := `{
1741+
"id": "ExistingMetadataFlag",
1742+
"description": "Feature flag with existing metadata",
1743+
"enabled": true,
1744+
"telemetry": {
1745+
"enabled": true,
1746+
"metadata": {
1747+
"customKey": "customValue",
1748+
"version": "1.0"
1749+
}
1750+
}
1751+
}`
1752+
1753+
eTag1 := "W/\"existing-etag\""
1754+
1755+
mockResponse := &settingsResponse{
1756+
settings: []azappconfig.Setting{
1757+
{
1758+
Key: toPtr(".appconfig.featureflag/ExistingMetadataFlag"),
1759+
Value: &value1,
1760+
ContentType: toPtr(featureFlagContentType),
1761+
ETag: (*azcore.ETag)(&eTag1),
1762+
},
1763+
},
1764+
pageETags: map[Selector][]*azcore.ETag{},
1765+
}
1766+
1767+
mockClient.On("getSettings", ctx).Return(mockResponse, nil)
1768+
1769+
azappcfg := &AzureAppConfiguration{
1770+
clientManager: &configurationClientManager{
1771+
staticClient: &configurationClientWrapper{client: &azappconfig.Client{}},
1772+
endpoint: "https://test.azconfig.io",
1773+
},
1774+
ffSelectors: getFeatureFlagSelectors([]Selector{}),
1775+
featureFlags: make(map[string]any),
1776+
tracingOptions: tracing.Options{
1777+
Enabled: true,
1778+
FeatureFlagTracing: &tracing.FeatureFlagTracing{},
1779+
},
1780+
}
1781+
1782+
err := azappcfg.loadFeatureFlags(ctx, mockClient)
1783+
assert.NoError(t, err)
1784+
1785+
// Verify feature flags structure
1786+
featureManagement := azappcfg.featureFlags[featureManagementSectionKey].(map[string]any)
1787+
featureFlags := featureManagement[featureFlagSectionKey].([]any)
1788+
assert.Len(t, featureFlags, 1)
1789+
1790+
flag := featureFlags[0].(map[string]any)
1791+
assert.Equal(t, "ExistingMetadataFlag", flag["id"])
1792+
1793+
telemetry := flag[telemetryKey].(map[string]any)
1794+
assert.True(t, telemetry[enabledKey].(bool))
1795+
1796+
metadata := telemetry[metadataKey].(map[string]any)
1797+
1798+
// Verify existing metadata is preserved
1799+
assert.Equal(t, "customValue", metadata["customKey"], "Existing custom metadata should be preserved")
1800+
assert.Equal(t, "1.0", metadata["version"], "Existing version metadata should be preserved")
1801+
1802+
// Verify new metadata is added
1803+
assert.Equal(t, "https://test.azconfig.io/kv/.appconfig.featureflag/ExistingMetadataFlag",
1804+
metadata[featureFlagReferenceKey], "Feature flag reference should be added to metadata")
1805+
}
1806+
1807+
func TestGenerateFeatureFlagReference(t *testing.T) {
1808+
tests := []struct {
1809+
name string
1810+
setting azappconfig.Setting
1811+
endpoint string
1812+
expected string
1813+
}{
1814+
{
1815+
name: "feature flag without label",
1816+
setting: azappconfig.Setting{
1817+
Key: toPtr(".appconfig.featureflag/TestFlag"),
1818+
Label: nil,
1819+
},
1820+
endpoint: "https://mystore.azconfig.io",
1821+
expected: "https://mystore.azconfig.io/kv/.appconfig.featureflag/TestFlag",
1822+
},
1823+
{
1824+
name: "feature flag with empty label",
1825+
setting: azappconfig.Setting{
1826+
Key: toPtr(".appconfig.featureflag/TestFlag"),
1827+
Label: toPtr(""),
1828+
},
1829+
endpoint: "https://mystore.azconfig.io",
1830+
expected: "https://mystore.azconfig.io/kv/.appconfig.featureflag/TestFlag",
1831+
},
1832+
{
1833+
name: "feature flag with whitespace-only label",
1834+
setting: azappconfig.Setting{
1835+
Key: toPtr(".appconfig.featureflag/TestFlag"),
1836+
Label: toPtr(" "),
1837+
},
1838+
endpoint: "https://mystore.azconfig.io",
1839+
expected: "https://mystore.azconfig.io/kv/.appconfig.featureflag/TestFlag",
1840+
},
1841+
{
1842+
name: "feature flag with valid label",
1843+
setting: azappconfig.Setting{
1844+
Key: toPtr(".appconfig.featureflag/TestFlag"),
1845+
Label: toPtr("production"),
1846+
},
1847+
endpoint: "https://mystore.azconfig.io",
1848+
expected: "https://mystore.azconfig.io/kv/.appconfig.featureflag/TestFlag?label=production",
1849+
},
1850+
{
1851+
name: "feature flag with label containing spaces",
1852+
setting: azappconfig.Setting{
1853+
Key: toPtr(".appconfig.featureflag/TestFlag"),
1854+
Label: toPtr("staging"),
1855+
},
1856+
endpoint: "https://test.azconfig.io",
1857+
expected: "https://test.azconfig.io/kv/.appconfig.featureflag/TestFlag?label=staging",
1858+
},
1859+
}
1860+
1861+
for _, test := range tests {
1862+
t.Run(test.name, func(t *testing.T) {
1863+
result := generateFeatureFlagReference(test.setting, test.endpoint)
1864+
assert.Equal(t, test.expected, result)
1865+
})
1866+
}
1867+
}

0 commit comments

Comments
 (0)