Skip to content
Draft
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
13 changes: 13 additions & 0 deletions azure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ hooks:
prepackage:
shell: pwsh
run: cd src && npm run build-client
predown:
windows:
shell: pwsh
run: |
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
./infra/scripts/pre-down.ps1
continueOnError: false
interactive: true
posix:
shell: sh
run: pwsh ./infra/scripts/pre-down.ps1
continueOnError: false
interactive: true
services:
QuoteOfTheDay:
project: src
Expand Down
54 changes: 53 additions & 1 deletion infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ param environmentName string

@secure()
param quoteOfTheDayDefinition object
param location string

param LAWname string
param location string
param LAWsku string
param AppInsightsName string
param ApplicationType string
Expand All @@ -20,6 +20,11 @@ param AACsoftDeleteRetentionInDays int
param AACenablePurgeProtection bool
param AACdisableLocalAuth bool

param principalId string
param principalType string = 'User'

param enableOnlineExperimentation bool

// Tags that should be applied to all resources.
//
// Note that 'azd-service-name' tags should be applied separately to service host resources.
Expand Down Expand Up @@ -48,6 +53,8 @@ module monitoring './shared/monitoring.bicep' = {
ApplicationType: ApplicationType
LAWsku: LAWsku
tags: tags
principalId: principalId
principalType: principalType
}
scope: rg
}
Expand All @@ -63,6 +70,7 @@ module appConfiguration './shared/appConfiguration.bicep' = {
name: '${AppConfigName}${resourceToken}'
applicationInsightsId: monitoring.outputs.applicationInsightsId
tags: tags
enableOnlineExperimentation: enableOnlineExperimentation
}
scope: rg
}
Expand Down Expand Up @@ -91,5 +99,49 @@ module quoteOfTheDay './app/QuoteOfTheDay.bicep' = {
scope: rg
}

// Setup for online experimentation if enabled
// Including adding summary rules and data export rule to Log Analytics
module onlineExperimentationWorkspace 'shared/onlineExperimentation.bicep' = if (enableOnlineExperimentation) {
name: 'online-experimentation-${resourceToken}'
scope: subscription()
params: {
resourceId: appConfiguration.outputs.onlineExperimentationResourceId
resourceGroupname: appConfiguration.outputs.managedResourceGroupName
principalId: principalId
principalType: principalType
}
}


var ruleDefinitions = loadYamlContent('shared/la-summary-rules.yaml')
module summaryRules 'shared/summaryRule.bicep' = [for (rule, i) in ruleDefinitions.summaryRules: if (enableOnlineExperimentation) {
name: 'loganalytics-summaryrule-${i}'
scope: rg
params: {
location: location
logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName
summaryRuleName: rule.name
description: rule.description
query: rule.query
binSize: rule.binSize
destinationTable: rule.destinationTable
}
}]

module dataExportRule 'shared/dataExport.bicep' = if (enableOnlineExperimentation) {
name: 'loganalytics-dataexportrule'
scope: rg
params: {
name: 'OEW-${resourceToken}-DataExportRule'
logAnalyticsWorkspaceName: monitoring.outputs.logAnalyticsWorkspaceName
storageAccountResourceId: appConfiguration.outputs.storageAccountResourceId
tables: [
'AppEvents'
]
}
}

output AZURE_RESOURCE_GROUP string = rg.name
output APPCONFIG_RESOURCE_NAME string = appConfiguration.outputs.appConfigurationName
output APPCONFIG_ENDPOINT string = appConfiguration.outputs.appConfigurationEndpoint
output APPLICATIONINSIGHTS_CONNECTION_STRING string = monitoring.outputs.applicationInsightsConnectionString
3 changes: 3 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@
},
"AACdisableLocalAuth": {
"value": false
},
"enableOnlineExperimentation": {
"value": "${ENABLE_ONLINE_EXPERIMENTATION=false}"
}
}
}
25 changes: 25 additions & 0 deletions infra/scripts/pre-down.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Get the directory of the current script
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition

# Check if user is logged in to Azure CLI
az account show --output none 2>$null
if ($LASTEXITCODE -ne 0) {
Write-Host "Not logged in to Azure CLI. Please run 'az login' first."
exit 1
}

# Load environment variables from azd env
$subscriptionId = azd env get-value AZURE_SUBSCRIPTION_ID
$resourceName = azd env get-value APPCONFIG_RESOURCE_NAME
$resourceGroup = azd env get-value AZURE_RESOURCE_GROUP
$experimentationResourceId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.AppConfiguration/configurationStores/$resourceName/experimentation/default"

# Check experimentation resource existence
az resource show --ids $experimentationResourceId --api-version 2025-02-01-preview --output none 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "Disabling online experimentation for App Configuration resource: $experimentationResourceId"
az resource delete --ids $experimentationResourceId --api-version 2025-02-01-preview
} else {
Write-Host "Online experimentation not enabled, skipping online experimentation disable step"
exit 0
}
10 changes: 10 additions & 0 deletions infra/shared/appConfiguration.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ param AACenablePurgeProtection bool
param AACdisableLocalAuth bool
param applicationInsightsId string
param tags object = {}
param enableOnlineExperimentation bool

resource appConfigurationStore 'Microsoft.AppConfiguration/configurationStores@2023-09-01-preview' = {
name: name
Expand Down Expand Up @@ -76,5 +77,14 @@ resource variantFeatureFlagGreeting 'Microsoft.AppConfiguration/configurationSto
}
}

resource experimentation 'Microsoft.AppConfiguration/configurationStores/experimentation@2025-02-01-preview' = if (enableOnlineExperimentation) {
parent: appConfigurationStore
name: 'default'
}

output appConfigurationEndpoint string = appConfigurationStore.properties.endpoint
output appConfigurationName string = appConfigurationStore.name

output managedResourceGroupName string = enableOnlineExperimentation ? experimentation.properties.managedResourceGroupName : ''
output onlineExperimentationResourceId string = enableOnlineExperimentation ? experimentation.properties.onlineExperimentationWorkspaceResourceId : ''
output storageAccountResourceId string = enableOnlineExperimentation ? experimentation.properties.storageAccountResourceId : ''
28 changes: 28 additions & 0 deletions infra/shared/dataExport.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
metadata description = 'Creates a data export rule.'
param name string
param logAnalyticsWorkspaceName string
param storageAccountResourceId string
param tables string[]

var segments = split(storageAccountResourceId, '/')
var storageAccountName = segments[length(segments) - 1]

resource storageAccount 'Microsoft.Storage/storageAccounts@2022-09-01' existing = {
name: storageAccountName
}

resource dataExportRule 'Microsoft.OperationalInsights/workspaces/dataExports@2020-08-01' = {
name: name
parent: logAnalyticsWorkspace
properties: {
destination: {
resourceId: storageAccount.id
}
enable: true
tableNames: tables
}
}

resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' existing = {
name: logAnalyticsWorkspaceName
}
33 changes: 33 additions & 0 deletions infra/shared/la-summary-rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
summaryRules:
- name: "Online-Experimentation-Assignment-Summary"
binSize: 20
destinationTable: OEWExperimentAssignmentSummary_CL
description: "Summary rule definition for online experiment assignment summary."
query: |
AppEvents
| where Name == "FeatureEvaluation"
| where Properties has "Percentile"
| where tostring(Properties.VariantAssignmentReason) == "Percentile"
| where tostring(Properties.TargetingId) != ""
| where tostring(Properties.Enabled) == "True"
| where tostring(Properties.AllocationId) != ""
| where toint(Properties.VariantAssignmentPercentage) < 100
| extend FeatureFlagReference = tostring(Properties.FeatureFlagReference)
| extend Label = tostring(parse_url(FeatureFlagReference).["Query Parameters"].label)
| project
TimeGenerated,
ItemCount,
FeatureFlagReference = FeatureFlagReference,
FeatureName = tostring(Properties.FeatureName),
Label = Label,
AllocationId = tostring(Properties.AllocationId),
Variant = tostring(Properties.Variant),
VariantAssignmentPercentage = toreal(Properties.VariantAssignmentPercentage),
IsControlVariant = tostring(Properties.DefaultWhenEnabled) == tostring(Properties.Variant)
| summarize
VariantAssignmentPercentage = take_any(VariantAssignmentPercentage),
IsControlVariant = take_any(IsControlVariant),
AssignmentEventCount = tolong(sum(ItemCount)),
FirstAssignmentTimestamp = min(TimeGenerated),
LastAssignmentTimestamp = max(TimeGenerated)
by FeatureFlagReference, FeatureName, Label, AllocationId, Variant
13 changes: 13 additions & 0 deletions infra/shared/monitoring.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ param ApplicationType string
param AIrequestSource string
param LAWsku string
param tags object = {}
param principalId string
param principalType string

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-preview' = {
name: logAnalyticsName
Expand All @@ -17,6 +19,17 @@ resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-12-01-previ
}
}

// Assign the Log Analytics Contributor role to the specified principal
var logAnalyticsContributorRoleId = '92aaf0da-9dab-42b6-94a3-d43ce8d16293'
resource role 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, resourceGroup().id, principalId, logAnalyticsContributorRoleId)
properties: {
principalId: principalId
principalType: principalType
roleDefinitionId: resourceId('Microsoft.Authorization/roleDefinitions', logAnalyticsContributorRoleId)
}
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
name: applicationInsightsName
location: location
Expand Down
22 changes: 22 additions & 0 deletions infra/shared/onlineExperimentation-access.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
metadata description = 'Configure online experimentation workspace access'
param resourceName string
param principalId string
param principalType string

resource workspace 'Microsoft.OnlineExperimentation/workspaces@2025-05-31-preview' existing = {
name: resourceName
}

resource onlineExperimentDataOwnerRole 'Microsoft.Authorization/roleAssignments@2022-04-01' existing = {
name: '53747cdd-e97c-477a-948c-b587d0e514b2' // Online Experimentation Data Owner
}

resource dataOwnerUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, resourceGroup().id, principalId, onlineExperimentDataOwnerRole.id)
scope: workspace
properties: {
principalId: principalId
roleDefinitionId: onlineExperimentDataOwnerRole.id
principalType: principalType
}
}
30 changes: 30 additions & 0 deletions infra/shared/onlineExperimentation.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
metadata description = 'Configure online experimentation workspace'
targetScope = 'subscription'
param resourceId string
param resourceGroupname string
param principalId string
param principalType string

resource managedResourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' existing = {
name: resourceGroupname
}

var segments = split(resourceId, '/')
var resourceName = segments[length(segments) - 1]

resource workspace 'Microsoft.OnlineExperimentation/workspaces@2025-05-31-preview' existing = {
name: resourceName
scope: managedResourceGroup
}

module workspaceAccess './onlineExperimentation-access.bicep' = {
name: 'online-experimentation-role-assignment'
scope: managedResourceGroup
params: {
resourceName: resourceName
principalId: principalId
principalType: principalType
}
}

output endpoint string = workspace.properties.endpoint
25 changes: 25 additions & 0 deletions infra/shared/summaryRule.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
metadata description = 'Creates a Log Analytics Summary Rule.'
param logAnalyticsWorkspaceName string
param summaryRuleName string
param description string
param ruleType string = 'User'
param query string
param binSize int
param destinationTable string
param location string

resource workspaceName_summaryRule 'Microsoft.OperationalInsights/workspaces/summaryLogs@2023-01-01-preview' = {
name: '${logAnalyticsWorkspaceName}/${summaryRuleName}'
location: location
properties: {
ruleType: ruleType
description: description
ruleDefinition: {
query: query
binSize: binSize
destinationTable: destinationTable
}
}
}

output id string = workspaceName_summaryRule.id
5 changes: 3 additions & 2 deletions src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
},
"dependencies": {
"@azure/app-configuration-provider": "latest",
"@microsoft/feature-management": "^2.0.0",
"@microsoft/feature-management-applicationinsights-node": "^2.0.0",
"@microsoft/feature-management": "^2.1.0",
"@microsoft/feature-management-applicationinsights-node": "^2.1.0",
"applicationinsights": "^2.9.6",
"dotenv": "^16.5.0",
"express": "^4.19.2"
}
}