Cookies: Provide a mechanism for per user control over cookies (#61566)

This commit is contained in:
Emil Tullstedt 2023-02-21 11:19:07 +01:00 committed by GitHub
parent 5eaaf9b9b7
commit 0caacb3333
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 207 additions and 97 deletions

View File

@ -91,6 +91,7 @@ Alpha features might be changed or removed without prior notice.
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view |
| `lokiQuerySplitting` | Split large interval queries into subqueries with smaller time intervals |
| `individualCookiePreferences` | Support overriding cookie preferences per user |
## Development feature toggles

View File

@ -81,4 +81,5 @@ export interface FeatureToggles {
logsSampleInExplore?: boolean;
logsContextDatasourceUi?: boolean;
lokiQuerySplitting?: boolean;
individualCookiePreferences?: boolean;
}

View File

@ -17,6 +17,7 @@ type UpdatePrefsCmd struct {
WeekStart string `json:"weekStart"`
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
Language string `json:"language"`
Cookies []pref.CookieType `json:"cookies,omitempty"`
}
// swagger:model
@ -32,4 +33,5 @@ type PatchPrefsCmd struct {
Language *string `json:"language,omitempty"`
QueryHistory *pref.QueryHistoryPreference `json:"queryHistory,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Cookies []pref.CookieType `json:"cookies,omitempty"`
}

View File

@ -122,13 +122,13 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
HelpEnabled: setting.HelpEnabled,
ProfileEnabled: setting.ProfileEnabled,
QueryHistoryEnabled: hs.Cfg.QueryHistoryEnabled,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleAnalytics4Id: setting.GoogleAnalytics4Id,
GoogleAnalytics4SendManualPageViews: setting.GoogleAnalytics4SendManualPageViews,
RudderstackWriteKey: setting.RudderstackWriteKey,
RudderstackDataPlaneUrl: setting.RudderstackDataPlaneUrl,
RudderstackSdkUrl: setting.RudderstackSdkUrl,
RudderstackConfigUrl: setting.RudderstackConfigUrl,
GoogleAnalyticsId: hs.Cfg.GoogleAnalyticsID,
GoogleAnalytics4Id: hs.Cfg.GoogleAnalytics4ID,
GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews,
RudderstackWriteKey: hs.Cfg.RudderstackWriteKey,
RudderstackDataPlaneUrl: hs.Cfg.RudderstackDataPlaneURL,
RudderstackSdkUrl: hs.Cfg.RudderstackSDKURL,
RudderstackConfigUrl: hs.Cfg.RudderstackConfigURL,
FeedbackLinksEnabled: hs.Cfg.FeedbackLinksEnabled,
ApplicationInsightsConnectionString: hs.Cfg.ApplicationInsightsConnectionString,
ApplicationInsightsEndpointUrl: hs.Cfg.ApplicationInsightsEndpointUrl,

View File

@ -48,6 +48,13 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
return nil, err
}
if hs.Features.IsEnabled(featuremgmt.FlagIndividualCookiePreferences) {
if !prefs.Cookies("analytics") {
settings.GoogleAnalytics4Id = ""
settings.GoogleAnalyticsId = ""
}
}
// Locale is used for some number and date/time formatting, whereas language is used just for
// translating words in the interface
acceptLangHeader := c.Req.Header.Get("Accept-Language")
@ -110,10 +117,10 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
Theme: prefs.Theme,
AppUrl: appURL,
AppSubUrl: appSubURL,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleAnalytics4Id: setting.GoogleAnalytics4Id,
GoogleAnalytics4SendManualPageViews: setting.GoogleAnalytics4SendManualPageViews,
GoogleTagManagerId: setting.GoogleTagManagerId,
GoogleAnalyticsId: settings.GoogleAnalyticsId,
GoogleAnalytics4Id: settings.GoogleAnalytics4Id,
GoogleAnalytics4SendManualPageViews: hs.Cfg.GoogleAnalytics4SendManualPageViews,
GoogleTagManagerId: hs.Cfg.GoogleTagManagerID,
BuildVersion: setting.BuildVersion,
BuildCommit: setting.BuildCommit,
NewGrafanaVersion: hs.grafanaUpdateChecker.LatestVersion(),

View File

@ -20,6 +20,7 @@ import (
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
loginservice "github.com/grafana/grafana/pkg/services/login"
pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -398,22 +399,35 @@ func (hs *HTTPServer) trySetEncryptedCookie(ctx *contextmodel.ReqContext, cookie
return nil
}
func (hs *HTTPServer) redirectWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) {
ctx.Logger.Warn(err.Error(), v...)
if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil {
hs.log.Error("Failed to set encrypted cookie", "err", err)
}
ctx.Redirect(hs.Cfg.AppSubURL + "/login")
func (hs *HTTPServer) redirectWithError(c *contextmodel.ReqContext, err error, v ...interface{}) {
c.Logger.Warn(err.Error(), v...)
c.Redirect(hs.redirectURLWithErrorCookie(c, err))
}
func (hs *HTTPServer) RedirectResponseWithError(ctx *contextmodel.ReqContext, err error, v ...interface{}) *response.RedirectResponse {
ctx.Logger.Error(err.Error(), v...)
if err := hs.trySetEncryptedCookie(ctx, loginErrorCookieName, getLoginExternalError(err), 60); err != nil {
hs.log.Error("Failed to set encrypted cookie", "err", err)
func (hs *HTTPServer) RedirectResponseWithError(c *contextmodel.ReqContext, err error, v ...interface{}) *response.RedirectResponse {
c.Logger.Error(err.Error(), v...)
location := hs.redirectURLWithErrorCookie(c, err)
return response.Redirect(location)
}
func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err error) string {
setCookie := true
if hs.Features.IsEnabled(featuremgmt.FlagIndividualCookiePreferences) {
prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: c.UserID, OrgID: c.OrgID, Teams: c.Teams}
prefs, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery)
if err != nil {
c.Redirect(hs.Cfg.AppSubURL + "/login")
}
setCookie = prefs.Cookies("functional")
}
return response.Redirect(hs.Cfg.AppSubURL + "/login")
if setCookie {
if err := hs.trySetEncryptedCookie(c, loginErrorCookieName, getLoginExternalError(err), 60); err != nil {
hs.log.Error("Failed to set encrypted cookie", "err", err)
}
}
return hs.Cfg.AppSubURL + "/login"
}
func (hs *HTTPServer) samlEnabled() bool {

View File

@ -27,15 +27,16 @@ import (
func setupSocialHTTPServerWithConfig(t *testing.T, cfg *setting.Cfg) *HTTPServer {
sqlStore := db.InitTestDB(t)
features := featuremgmt.WithFeatures()
return &HTTPServer{
Cfg: cfg,
License: &licensing.OSSLicensingService{Cfg: cfg},
SQLStore: sqlStore,
SocialService: social.ProvideService(cfg, featuremgmt.WithFeatures(), &usagestats.UsageStatsMock{}),
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}),
HooksService: hooks.ProvideService(),
SecretsService: fakes.NewFakeSecretsService(),
Features: featuremgmt.WithFeatures(),
Features: features,
}
}

View File

@ -39,7 +39,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp
} else {
queryResult, err := hs.DashboardService.GetDashboard(c.Req.Context(), &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
dashboardID = queryResult.ID
}
@ -48,7 +48,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp
cmd.HomeDashboardID = dashboardID
if err := hs.preferenceService.Save(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to set home dashboard", err)
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to set home dashboard", err)
}
return response.Success("Home dashboard set")
@ -71,7 +71,7 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
preference, err := hs.preferenceService.Get(ctx, &prefsQuery)
if err != nil {
return response.Error(500, "Failed to get preferences", err)
return response.Error(http.StatusInternalServerError, "Failed to get preferences", err)
}
var dashboardUID string
@ -148,7 +148,7 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t
} else {
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
dashboardID = queryResult.ID
}
@ -156,19 +156,20 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t
dtoCmd.HomeDashboardID = dashboardID
saveCmd := pref.SavePreferenceCommand{
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Language: dtoCmd.Language,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
QueryHistory: dtoCmd.QueryHistory,
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Language: dtoCmd.Language,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
}
if err := hs.preferenceService.Save(ctx, &saveCmd); err != nil {
return response.Error(500, "Failed to save preferences", err)
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save preferences", err)
}
return response.Success("Preferences updated")
@ -193,7 +194,7 @@ func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response.
func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.PatchPrefsCmd) response.Response {
if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme && *dtoCmd.Theme != systemTheme {
return response.Error(400, "Invalid theme", nil)
return response.Error(http.StatusBadRequest, "Invalid theme", nil)
}
// convert dashboard UID to ID in order to store internally if it exists in the query, otherwise take the id from query
@ -207,7 +208,7 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
} else {
queryResult, err := hs.DashboardService.GetDashboard(ctx, &query)
if err != nil {
return response.Error(404, "Dashboard not found", err)
return response.Error(http.StatusNotFound, "Dashboard not found", err)
}
dashboardID = &queryResult.ID
}
@ -215,19 +216,20 @@ func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, te
dtoCmd.HomeDashboardID = dashboardID
patchCmd := pref.PatchPreferenceCommand{
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
Language: dtoCmd.Language,
QueryHistory: dtoCmd.QueryHistory,
UserID: userID,
OrgID: orgID,
TeamID: teamId,
Theme: dtoCmd.Theme,
Timezone: dtoCmd.Timezone,
WeekStart: dtoCmd.WeekStart,
HomeDashboardID: dtoCmd.HomeDashboardID,
Language: dtoCmd.Language,
QueryHistory: dtoCmd.QueryHistory,
CookiePreferences: dtoCmd.Cookies,
}
if err := hs.preferenceService.Patch(ctx, &patchCmd); err != nil {
return response.Error(500, "Failed to save preferences", err)
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to save preferences", err)
}
return response.Success("Preferences updated")

View File

@ -367,5 +367,10 @@ var (
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "individualCookiePreferences",
Description: "Support overriding cookie preferences per user",
State: FeatureStateAlpha,
},
}
)

View File

@ -266,4 +266,8 @@ const (
// FlagLokiQuerySplitting
// Split large interval queries into subqueries with smaller time intervals
FlagLokiQuerySplitting = "lokiQuerySplitting"
// FlagIndividualCookiePreferences
// Support overriding cookie preferences per user
FlagIndividualCookiePreferences = "individualCookiePreferences"
)

View File

@ -7,9 +7,16 @@ import (
"errors"
"fmt"
"time"
"github.com/grafana/grafana/pkg/util/errutil"
)
var ErrPrefNotFound = errors.New("preference not found")
var ErrUnknownCookieType = errutil.NewBase(
errutil.StatusBadRequest,
"preferences.unknownCookieType",
errutil.WithPublicMessage("Got an unknown cookie preference type. Expected a set containing one or more of 'functional', 'performance', or 'analytics'}"),
)
type Preference struct {
ID int64 `xorm:"pk autoincr 'id'" db:"id"`
@ -27,6 +34,15 @@ type Preference struct {
JSONData *PreferenceJSONData `xorm:"json_data" db:"json_data"`
}
func (p Preference) Cookies(typ string) bool {
if p.JSONData == nil || p.JSONData.CookiePreferences == nil {
return false
}
_, ok := p.JSONData.CookiePreferences[typ]
return ok
}
type GetPreferenceWithDefaultsQuery struct {
Teams []int64
OrgID int64
@ -44,13 +60,14 @@ type SavePreferenceCommand struct {
OrgID int64
TeamID int64
HomeDashboardID int64 `json:"homeDashboardId,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Timezone string `json:"timezone,omitempty"`
WeekStart string `json:"weekStart,omitempty"`
Theme string `json:"theme,omitempty"`
Language string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
HomeDashboardID int64 `json:"homeDashboardId,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Timezone string `json:"timezone,omitempty"`
WeekStart string `json:"weekStart,omitempty"`
Theme string `json:"theme,omitempty"`
Language string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
CookiePreferences []CookieType `json:"cookiePreferences,omitempty"`
}
type PatchPreferenceCommand struct {
@ -58,18 +75,20 @@ type PatchPreferenceCommand struct {
OrgID int64
TeamID int64
HomeDashboardID *int64 `json:"homeDashboardId,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Timezone *string `json:"timezone,omitempty"`
WeekStart *string `json:"weekStart,omitempty"`
Theme *string `json:"theme,omitempty"`
Language *string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
HomeDashboardID *int64 `json:"homeDashboardId,omitempty"`
HomeDashboardUID *string `json:"homeDashboardUID,omitempty"`
Timezone *string `json:"timezone,omitempty"`
WeekStart *string `json:"weekStart,omitempty"`
Theme *string `json:"theme,omitempty"`
Language *string `json:"language,omitempty"`
QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"`
CookiePreferences []CookieType `json:"cookiePreferences,omitempty"`
}
type PreferenceJSONData struct {
Language string `json:"language"`
QueryHistory QueryHistoryPreference `json:"queryHistory"`
Language string `json:"language"`
QueryHistory QueryHistoryPreference `json:"queryHistory"`
CookiePreferences map[string]struct{} `json:"cookiePreferences"`
}
type QueryHistoryPreference struct {
@ -112,3 +131,7 @@ func (j *PreferenceJSONData) ToDB() ([]byte, error) {
}
func (p Preference) TableName() string { return "preferences" }
// swagger:model
// Enum: analytics,performance,functional
type CookieType string

View File

@ -68,6 +68,10 @@ func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreference
if p.JSONData.QueryHistory.HomeTab != "" {
res.JSONData.QueryHistory.HomeTab = p.JSONData.QueryHistory.HomeTab
}
if p.JSONData.CookiePreferences != nil {
res.JSONData.CookiePreferences = p.JSONData.CookiePreferences
}
}
}
@ -91,6 +95,11 @@ func (s *Service) Get(ctx context.Context, query *pref.GetPreferenceQuery) (*pre
}
func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) error {
jsonData, err := preferenceData(cmd)
if err != nil {
return err
}
preference, err := s.store.Get(ctx, &pref.Preference{
OrgID: cmd.OrgID,
UserID: cmd.UserID,
@ -108,9 +117,7 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
Theme: cmd.Theme,
Created: time.Now(),
Updated: time.Now(),
JSONData: &pref.PreferenceJSONData{
Language: cmd.Language,
},
JSONData: jsonData,
}
_, err = s.store.Insert(ctx, preference)
if err != nil {
@ -126,13 +133,8 @@ func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) err
preference.Updated = time.Now()
preference.Version += 1
preference.HomeDashboardID = cmd.HomeDashboardID
preference.JSONData = &pref.PreferenceJSONData{
Language: cmd.Language,
}
preference.JSONData = jsonData
if cmd.QueryHistory != nil {
preference.JSONData.QueryHistory = *cmd.QueryHistory
}
return s.store.Update(ctx, preference)
}
@ -179,6 +181,18 @@ func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) e
preference.HomeDashboardID = *cmd.HomeDashboardID
}
if cmd.CookiePreferences != nil {
cookies, err := parseCookiePreferences(cmd.CookiePreferences)
if err != nil {
return err
}
if preference.JSONData == nil {
preference.JSONData = &pref.PreferenceJSONData{}
}
preference.JSONData.CookiePreferences = cookies
}
if cmd.Timezone != nil {
preference.Timezone = *cmd.Timezone
}
@ -221,3 +235,40 @@ func (s *Service) GetDefaults() *pref.Preference {
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
}
func parseCookiePreferences(prefs []pref.CookieType) (map[string]struct{}, error) {
allowed := map[pref.CookieType]struct{}{
"analytics": {},
"performance": {},
"functional": {},
}
m := map[string]struct{}{}
for _, c := range prefs {
if _, ok := allowed[c]; !ok {
return nil, pref.ErrUnknownCookieType.Errorf("'%s' is not an allowed cookie type", c)
}
m[string(c)] = struct{}{}
}
return m, nil
}
func preferenceData(cmd *pref.SavePreferenceCommand) (*pref.PreferenceJSONData, error) {
jsonData := &pref.PreferenceJSONData{
Language: cmd.Language,
}
if cmd.QueryHistory != nil {
jsonData.QueryHistory = *cmd.QueryHistory
}
if cmd.CookiePreferences != nil {
cookies, err := parseCookiePreferences(cmd.CookiePreferences)
if err != nil {
return nil, err
}
jsonData.CookiePreferences = cookies
}
return jsonData, nil
}

View File

@ -22,12 +22,11 @@ import (
"time"
"github.com/gobwas/glob"
"github.com/prometheus/common/model"
"gopkg.in/ini.v1"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-azure-sdk-go/azsettings"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/prometheus/common/model"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/util"
@ -130,16 +129,6 @@ var (
appliedCommandLineProperties []string
appliedEnvOverrides []string
// analytics
GoogleAnalyticsId string
GoogleAnalytics4Id string
GoogleAnalytics4SendManualPageViews bool
GoogleTagManagerId string
RudderstackDataPlaneUrl string
RudderstackWriteKey string
RudderstackSdkUrl string
RudderstackConfigUrl string
// Alerting
AlertingEnabled *bool
ExecuteAlerts bool
@ -419,6 +408,16 @@ type Cfg struct {
ApplicationInsightsEndpointUrl string
FeedbackLinksEnabled bool
// Frontend analytics
GoogleAnalyticsID string
GoogleAnalytics4ID string
GoogleAnalytics4SendManualPageViews bool
GoogleTagManagerID string
RudderstackDataPlaneURL string
RudderstackWriteKey string
RudderstackSDKURL string
RudderstackConfigURL string
// AzureAD
AzureADSkipOrgRoleSync bool
@ -1036,15 +1035,15 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
analytics := iniFile.Section("analytics")
cfg.CheckForGrafanaUpdates = analytics.Key("check_for_updates").MustBool(true)
cfg.CheckForPluginUpdates = analytics.Key("check_for_plugin_updates").MustBool(true)
GoogleAnalyticsId = analytics.Key("google_analytics_ua_id").String()
GoogleAnalytics4Id = analytics.Key("google_analytics_4_id").String()
GoogleAnalytics4SendManualPageViews = analytics.Key("google_analytics_4_send_manual_page_views").MustBool(false)
GoogleTagManagerId = analytics.Key("google_tag_manager_id").String()
RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
RudderstackDataPlaneUrl = analytics.Key("rudderstack_data_plane_url").String()
RudderstackSdkUrl = analytics.Key("rudderstack_sdk_url").String()
RudderstackConfigUrl = analytics.Key("rudderstack_config_url").String()
cfg.GoogleAnalyticsID = analytics.Key("google_analytics_ua_id").String()
cfg.GoogleAnalytics4ID = analytics.Key("google_analytics_4_id").String()
cfg.GoogleAnalytics4SendManualPageViews = analytics.Key("google_analytics_4_send_manual_page_views").MustBool(false)
cfg.GoogleTagManagerID = analytics.Key("google_tag_manager_id").String()
cfg.RudderstackWriteKey = analytics.Key("rudderstack_write_key").String()
cfg.RudderstackDataPlaneURL = analytics.Key("rudderstack_data_plane_url").String()
cfg.RudderstackSDKURL = analytics.Key("rudderstack_sdk_url").String()
cfg.RudderstackConfigURL = analytics.Key("rudderstack_config_url").String()
cfg.ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true)
cfg.ReportingDistributor = analytics.Key("reporting_distributor").MustString("grafana-labs")