From c780c0b1682e38f90eb2e6c25b7a9772b353d6cb Mon Sep 17 00:00:00 2001 From: Ayu Date: Mon, 12 Aug 2024 11:42:15 +0300 Subject: [PATCH] feat(core): dynamically update script.js based on user configuration (#119) * feat: convert to array of script features * feat: tracker choices send patch request from client * feat: serve script file dynamically off runtime config * perf: move script files to the hot path * fix(core): update request object when modifying script path as well --- bun.lockb | Bin 370640 -> 370640 bytes core/api/oas_defaults_gen.go | 4 - core/api/oas_json_gen.go | 78 ++++++---------- core/api/oas_schemas_gen.go | 88 +++++------------- core/api/oas_validators_gen.go | 26 +++++- core/cmd/start.go | 2 +- core/openapi.yaml | 8 +- core/services/assets.go | 55 ++++++++--- core/services/oas.go | 47 +++++++--- core/services/users.go | 31 ++++-- dashboard/app/api/types.d.ts | 8 +- .../components/settings/Sidebar.module.css | 2 + dashboard/app/routes/settings.tracker.tsx | 37 ++++++-- tracker/Taskfile.yaml | 8 +- 14 files changed, 217 insertions(+), 177 deletions(-) diff --git a/bun.lockb b/bun.lockb index 61c01b49e720933d842657a50149988452041374..ed87024a4bb4935f0a40293ef51fd9059b645ef5 100644 GIT binary patch delta 33 pcmca`UhKkov4$4L7N#xCo_E+8;|%o-^^DpB?l5l;xWm$L8UW+X4G;hT delta 33 lcmca`UhKkov4$4L7N#xCo_E-p7{H)C;12WlfIBP=rvcH>3&Q{a diff --git a/core/api/oas_defaults_gen.go b/core/api/oas_defaults_gen.go index 6d1e3983..3460a8e1 100644 --- a/core/api/oas_defaults_gen.go +++ b/core/api/oas_defaults_gen.go @@ -56,8 +56,4 @@ func (s *UserSettings) setDefaults() { val := UserSettingsLanguage("en") s.Language.SetTo(val) } - { - val := UserSettingsScriptType("default") - s.ScriptType.SetTo(val) - } } diff --git a/core/api/oas_json_gen.go b/core/api/oas_json_gen.go index 0a2eba46..c86bd71d 100644 --- a/core/api/oas_json_gen.go +++ b/core/api/oas_json_gen.go @@ -2112,39 +2112,6 @@ func (s *OptUserSettingsLanguage) UnmarshalJSON(data []byte) error { return s.Decode(d) } -// Encode encodes UserSettingsScriptType as json. -func (o OptUserSettingsScriptType) Encode(e *jx.Encoder) { - if !o.Set { - return - } - e.Str(string(o.Value)) -} - -// Decode decodes UserSettingsScriptType from json. -func (o *OptUserSettingsScriptType) Decode(d *jx.Decoder) error { - if o == nil { - return errors.New("invalid: unable to decode OptUserSettingsScriptType to nil") - } - o.Set = true - if err := o.Value.Decode(d); err != nil { - return err - } - return nil -} - -// MarshalJSON implements stdjson.Marshaler. -func (s OptUserSettingsScriptType) MarshalJSON() ([]byte, error) { - e := jx.Encoder{} - s.Encode(&e) - return e.Bytes(), nil -} - -// UnmarshalJSON implements stdjson.Unmarshaler. -func (s *OptUserSettingsScriptType) UnmarshalJSON(data []byte) error { - d := jx.DecodeBytes(data) - return s.Decode(d) -} - // Encode encodes WebsiteGetSummary as json. func (o OptWebsiteGetSummary) Encode(e *jx.Encoder) { if !o.Set { @@ -5646,9 +5613,13 @@ func (s *UserSettings) encodeFields(e *jx.Encoder) { } } { - if s.ScriptType.Set { + if s.ScriptType != nil { e.FieldStart("script_type") - s.ScriptType.Encode(e) + e.ArrStart() + for _, elem := range s.ScriptType { + elem.Encode(e) + } + e.ArrEnd() } } } @@ -5679,8 +5650,15 @@ func (s *UserSettings) Decode(d *jx.Decoder) error { } case "script_type": if err := func() error { - s.ScriptType.Reset() - if err := s.ScriptType.Decode(d); err != nil { + s.ScriptType = make([]UserSettingsScriptTypeItem, 0) + if err := d.Arr(func(d *jx.Decoder) error { + var elem UserSettingsScriptTypeItem + if err := elem.Decode(d); err != nil { + return err + } + s.ScriptType = append(s.ScriptType, elem) + return nil + }); err != nil { return err } return nil @@ -5749,42 +5727,42 @@ func (s *UserSettingsLanguage) UnmarshalJSON(data []byte) error { return s.Decode(d) } -// Encode encodes UserSettingsScriptType as json. -func (s UserSettingsScriptType) Encode(e *jx.Encoder) { +// Encode encodes UserSettingsScriptTypeItem as json. +func (s UserSettingsScriptTypeItem) Encode(e *jx.Encoder) { e.Str(string(s)) } -// Decode decodes UserSettingsScriptType from json. -func (s *UserSettingsScriptType) Decode(d *jx.Decoder) error { +// Decode decodes UserSettingsScriptTypeItem from json. +func (s *UserSettingsScriptTypeItem) Decode(d *jx.Decoder) error { if s == nil { - return errors.New("invalid: unable to decode UserSettingsScriptType to nil") + return errors.New("invalid: unable to decode UserSettingsScriptTypeItem to nil") } v, err := d.StrBytes() if err != nil { return err } // Try to use constant string. - switch UserSettingsScriptType(v) { - case UserSettingsScriptTypeDefault: - *s = UserSettingsScriptTypeDefault - case UserSettingsScriptTypeTaggedEvents: - *s = UserSettingsScriptTypeTaggedEvents + switch UserSettingsScriptTypeItem(v) { + case UserSettingsScriptTypeItemDefault: + *s = UserSettingsScriptTypeItemDefault + case UserSettingsScriptTypeItemTaggedEvents: + *s = UserSettingsScriptTypeItemTaggedEvents default: - *s = UserSettingsScriptType(v) + *s = UserSettingsScriptTypeItem(v) } return nil } // MarshalJSON implements stdjson.Marshaler. -func (s UserSettingsScriptType) MarshalJSON() ([]byte, error) { +func (s UserSettingsScriptTypeItem) MarshalJSON() ([]byte, error) { e := jx.Encoder{} s.Encode(&e) return e.Bytes(), nil } // UnmarshalJSON implements stdjson.Unmarshaler. -func (s *UserSettingsScriptType) UnmarshalJSON(data []byte) error { +func (s *UserSettingsScriptTypeItem) UnmarshalJSON(data []byte) error { d := jx.DecodeBytes(data) return s.Decode(d) } diff --git a/core/api/oas_schemas_gen.go b/core/api/oas_schemas_gen.go index c29a62ed..3cf664c2 100644 --- a/core/api/oas_schemas_gen.go +++ b/core/api/oas_schemas_gen.go @@ -1390,52 +1390,6 @@ func (o OptUserSettingsLanguage) Or(d UserSettingsLanguage) UserSettingsLanguage return d } -// NewOptUserSettingsScriptType returns new OptUserSettingsScriptType with value set to v. -func NewOptUserSettingsScriptType(v UserSettingsScriptType) OptUserSettingsScriptType { - return OptUserSettingsScriptType{ - Value: v, - Set: true, - } -} - -// OptUserSettingsScriptType is optional UserSettingsScriptType. -type OptUserSettingsScriptType struct { - Value UserSettingsScriptType - Set bool -} - -// IsSet returns true if OptUserSettingsScriptType was set. -func (o OptUserSettingsScriptType) IsSet() bool { return o.Set } - -// Reset unsets value. -func (o *OptUserSettingsScriptType) Reset() { - var v UserSettingsScriptType - o.Value = v - o.Set = false -} - -// SetTo sets value to v. -func (o *OptUserSettingsScriptType) SetTo(v UserSettingsScriptType) { - o.Set = true - o.Value = v -} - -// Get returns value and boolean that denotes whether value was set. -func (o OptUserSettingsScriptType) Get() (v UserSettingsScriptType, ok bool) { - if !o.Set { - return v, false - } - return o.Value, true -} - -// Or returns value if set, or given parameter if does not. -func (o OptUserSettingsScriptType) Or(d UserSettingsScriptType) UserSettingsScriptType { - if v, ok := o.Get(); ok { - return v - } - return d -} - // NewOptWebsiteGetSummary returns new OptWebsiteGetSummary with value set to v. func NewOptWebsiteGetSummary(v WebsiteGetSummary) OptWebsiteGetSummary { return OptWebsiteGetSummary{ @@ -2641,8 +2595,8 @@ func (s *UserPatch) SetSettings(val OptUserSettings) { // Response body for getting user settings. // Ref: #/components/schemas/UserSettings type UserSettings struct { - Language OptUserSettingsLanguage `json:"language"` - ScriptType OptUserSettingsScriptType `json:"script_type"` + Language OptUserSettingsLanguage `json:"language"` + ScriptType []UserSettingsScriptTypeItem `json:"script_type"` } // GetLanguage returns the value of Language. @@ -2651,7 +2605,7 @@ func (s *UserSettings) GetLanguage() OptUserSettingsLanguage { } // GetScriptType returns the value of ScriptType. -func (s *UserSettings) GetScriptType() OptUserSettingsScriptType { +func (s *UserSettings) GetScriptType() []UserSettingsScriptTypeItem { return s.ScriptType } @@ -2661,7 +2615,7 @@ func (s *UserSettings) SetLanguage(val OptUserSettingsLanguage) { } // SetScriptType sets the value of ScriptType. -func (s *UserSettings) SetScriptType(val OptUserSettingsScriptType) { +func (s *UserSettings) SetScriptType(val []UserSettingsScriptTypeItem) { s.ScriptType = val } @@ -2699,27 +2653,27 @@ func (s *UserSettingsLanguage) UnmarshalText(data []byte) error { } } -type UserSettingsScriptType string +type UserSettingsScriptTypeItem string const ( - UserSettingsScriptTypeDefault UserSettingsScriptType = "default" - UserSettingsScriptTypeTaggedEvents UserSettingsScriptType = "tagged-events" + UserSettingsScriptTypeItemDefault UserSettingsScriptTypeItem = "default" + UserSettingsScriptTypeItemTaggedEvents UserSettingsScriptTypeItem = "tagged-events" ) -// AllValues returns all UserSettingsScriptType values. -func (UserSettingsScriptType) AllValues() []UserSettingsScriptType { - return []UserSettingsScriptType{ - UserSettingsScriptTypeDefault, - UserSettingsScriptTypeTaggedEvents, +// AllValues returns all UserSettingsScriptTypeItem values. +func (UserSettingsScriptTypeItem) AllValues() []UserSettingsScriptTypeItem { + return []UserSettingsScriptTypeItem{ + UserSettingsScriptTypeItemDefault, + UserSettingsScriptTypeItemTaggedEvents, } } // MarshalText implements encoding.TextMarshaler. -func (s UserSettingsScriptType) MarshalText() ([]byte, error) { +func (s UserSettingsScriptTypeItem) MarshalText() ([]byte, error) { switch s { - case UserSettingsScriptTypeDefault: + case UserSettingsScriptTypeItemDefault: return []byte(s), nil - case UserSettingsScriptTypeTaggedEvents: + case UserSettingsScriptTypeItemTaggedEvents: return []byte(s), nil default: return nil, errors.Errorf("invalid value: %q", s) @@ -2727,13 +2681,13 @@ func (s UserSettingsScriptType) MarshalText() ([]byte, error) { } // UnmarshalText implements encoding.TextUnmarshaler. -func (s *UserSettingsScriptType) UnmarshalText(data []byte) error { - switch UserSettingsScriptType(data) { - case UserSettingsScriptTypeDefault: - *s = UserSettingsScriptTypeDefault +func (s *UserSettingsScriptTypeItem) UnmarshalText(data []byte) error { + switch UserSettingsScriptTypeItem(data) { + case UserSettingsScriptTypeItemDefault: + *s = UserSettingsScriptTypeItemDefault return nil - case UserSettingsScriptTypeTaggedEvents: - *s = UserSettingsScriptTypeTaggedEvents + case UserSettingsScriptTypeItemTaggedEvents: + *s = UserSettingsScriptTypeItemTaggedEvents return nil default: return errors.Errorf("invalid value: %q", data) diff --git a/core/api/oas_validators_gen.go b/core/api/oas_validators_gen.go index 9a96aed7..4b212c0a 100644 --- a/core/api/oas_validators_gen.go +++ b/core/api/oas_validators_gen.go @@ -1167,16 +1167,34 @@ func (s *UserSettings) Validate() error { }) } if err := func() error { - if value, ok := s.ScriptType.Get(); ok { + if err := (validate.Array{ + MinLength: 0, + MinLengthSet: false, + MaxLength: 0, + MaxLengthSet: false, + }).ValidateLength(len(s.ScriptType)); err != nil { + return errors.Wrap(err, "array") + } + if err := validate.UniqueItems(s.ScriptType); err != nil { + return errors.Wrap(err, "array") + } + var failures []validate.FieldError + for i, elem := range s.ScriptType { if err := func() error { - if err := value.Validate(); err != nil { + if err := elem.Validate(); err != nil { return err } return nil }(); err != nil { - return err + failures = append(failures, validate.FieldError{ + Name: fmt.Sprintf("[%d]", i), + Error: err, + }) } } + if len(failures) > 0 { + return &validate.Error{Fields: failures} + } return nil }(); err != nil { failures = append(failures, validate.FieldError{ @@ -1199,7 +1217,7 @@ func (s UserSettingsLanguage) Validate() error { } } -func (s UserSettingsScriptType) Validate() error { +func (s UserSettingsScriptTypeItem) Validate() error { switch s { case "default": return nil diff --git a/core/cmd/start.go b/core/cmd/start.go index 070f5079..2a8232b6 100644 --- a/core/cmd/start.go +++ b/core/cmd/start.go @@ -151,7 +151,7 @@ func (s *StartCommand) Run(ctx context.Context) error { mux.Handle("/api/", http.StripPrefix("/api", apiHandler)) // SPA client. - err = services.SetupAssetHandler(mux) + err = services.SetupAssetHandler(mux, service.RuntimeConfig) if err != nil { return errors.Wrap(err, "failed to setup asset handler") } diff --git a/core/openapi.yaml b/core/openapi.yaml index 6e7048f7..9e679529 100644 --- a/core/openapi.yaml +++ b/core/openapi.yaml @@ -1455,9 +1455,11 @@ components: enum: [en] default: en script_type: - type: string - enum: [default, tagged-events] - default: default + type: array + items: + type: string + enum: [default, tagged-events] + uniqueItems: true UserGet: type: object title: UserGet diff --git a/core/services/assets.go b/core/services/assets.go index f1570eda..7aaf8e0b 100644 --- a/core/services/assets.go +++ b/core/services/assets.go @@ -20,15 +20,17 @@ type SPAHandler struct { indexFile []byte indexETag string fileETags map[string]string + + runtimeConfig *RuntimeConfig } -func SetupAssetHandler(mux *http.ServeMux) error { +func SetupAssetHandler(mux *http.ServeMux, runtimeConfig *RuntimeConfig) error { client, err := generate.SPAClient() if err != nil { return errors.Wrap(err, "failed to create spa client") } - handler, err := NewSPAHandler(client) + handler, err := NewSPAHandler(client, runtimeConfig) if err != nil { return err } @@ -37,7 +39,7 @@ func SetupAssetHandler(mux *http.ServeMux) error { return nil } -func NewSPAHandler(client fs.FS) (*SPAHandler, error) { +func NewSPAHandler(client fs.FS, runtimeConfig *RuntimeConfig) (*SPAHandler, error) { clientServer := http.FileServer(http.FS(client)) indexFile, err := readFile(client, "index.html") @@ -50,6 +52,8 @@ func NewSPAHandler(client fs.FS) (*SPAHandler, error) { indexFile: indexFile, indexETag: generateETag(indexFile), fileETags: make(map[string]string), + + runtimeConfig: runtimeConfig, } if err := handler.precomputeFileETags(client); err != nil { @@ -64,7 +68,7 @@ func (h *SPAHandler) precomputeFileETags(client fs.FS) error { if err != nil { return err } - if !d.IsDir() && (isAssetPath("/"+path) || isRootFile(path)) { + if !d.IsDir() && (isAssetPath("/"+path) || isRootFile(path) || isScriptFile("/"+path)) { content, err := readFile(client, path) if err != nil { return err @@ -79,12 +83,12 @@ func (h *SPAHandler) serveFile(w http.ResponseWriter, r *http.Request, filePath if etag, ok := h.fileETags[filePath]; ok { w.Header().Set("ETag", etag) - // 1 year for most asset files. - cacheControl := "public, max-age=31536000, immutable" + // 6 hours, 24 hours for root favicon files and tracker script. + cacheControl := "public, max-age=21600, stale-while-revalidate=86400" - // 24 hours for root favicon files and tracker script. - if isRootFile(strings.TrimPrefix(filePath, "/")) { - cacheControl = "public, max-age=86400, must-revalidate" + if !isScriptFile(filePath) && !isRootFile(strings.TrimPrefix(filePath, "/")) { + // 1 year for most asset files. + cacheControl = "public, max-age=31536000, immutable" } w.Header().Set("Cache-Control", cacheControl) @@ -108,15 +112,34 @@ func (h *SPAHandler) serveFile(w http.ResponseWriter, r *http.Request, filePath func (h *SPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { uPath := path.Clean(r.URL.Path) - _, exists := h.fileETags[uPath] - // Serve index.html to all routes that are not /api. - if uPath == "/" || !exists { - h.serveIndexHTML(w, r) + // Check if the request is for script.js or any file in the /scripts/ directory + if uPath == "/script.js" || strings.HasPrefix(uPath, "/scripts/") { + var scriptFile string + if uPath == "/script.js" { + if h.runtimeConfig.ScriptType.TaggedEvent { + scriptFile = "/scripts/tagged-events.js" + } else { + scriptFile = "/scripts/default.js" + } + } else { + scriptFile = uPath + } + + // Update the request URL to match the actual file being served + r.URL.Path = scriptFile + h.serveFile(w, r, scriptFile) return } - h.serveFile(w, r, uPath) + // Check if the file exists in our precomputed ETags + if _, exists := h.fileETags[uPath]; exists { + h.serveFile(w, r, uPath) + return + } + + // Serve index.html for all other routes that are not /api + h.serveIndexHTML(w, r) } func (h *SPAHandler) serveIndexHTML(w http.ResponseWriter, r *http.Request) { @@ -144,6 +167,10 @@ func isRootFile(path string) bool { return !strings.Contains(path, "/") && path != "index.html" } +func isScriptFile(path string) bool { + return strings.HasPrefix(path, "/scripts/") +} + func readFile(filesystem fs.FS, file string) ([]byte, error) { f, err := filesystem.Open(file) if err != nil { diff --git a/core/services/oas.go b/core/services/oas.go index dcdbebcc..c2fa7b89 100644 --- a/core/services/oas.go +++ b/core/services/oas.go @@ -2,6 +2,7 @@ package services import ( "context" + "strings" "github.com/go-faster/errors" "github.com/medama-io/go-referrer-parser" @@ -11,19 +12,21 @@ import ( "github.com/medama-io/medama/db/sqlite" "github.com/medama-io/medama/model" "github.com/medama-io/medama/util" + "github.com/medama-io/medama/util/logger" ) +type ScriptType struct { + // Default script that collects page view data. + Default bool + // Script that collects page view data and custom event properties. + TaggedEvent bool +} + // This is a runtime config that is read from user settings in the database. type RuntimeConfig struct { // Tracker settings. - // Choose what type of script to serve from /script.js. - // - // Options: - // - // - "default" - Default script that collects page view data. - // - // - "tagged-events" - Script that collects page view data and custom event properties. - ScriptType string + // Choose what features of script to serve from /script.js. + ScriptType ScriptType } type Handler struct { @@ -41,7 +44,7 @@ type Handler struct { hostnames *util.CacheStore // Runtime config - runtimeConfig *RuntimeConfig + RuntimeConfig *RuntimeConfig } // NewService returns a new instance of the ogen service handler. @@ -85,7 +88,7 @@ func NewService(ctx context.Context, auth *util.AuthService, sqlite *sqlite.Clie timezoneMap: &tzMap, codeCountryMap: &codeCountryMap, hostnames: &hostnameCache, - runtimeConfig: runtimeConfig, + RuntimeConfig: runtimeConfig, }, nil } @@ -98,7 +101,7 @@ func NewRuntimeConfig(ctx context.Context, user *sqlite.Client, analytics *duckd } return &RuntimeConfig{ - ScriptType: settings.ScriptType, + ScriptType: convertScriptType(settings.ScriptType), }, nil } @@ -108,8 +111,28 @@ func (r *RuntimeConfig) UpdateConfig(ctx context.Context, meta *sqlite.Client, s if err != nil { return errors.Wrap(err, "script type update config") } - r.ScriptType = settings.ScriptType + r.ScriptType = convertScriptType(settings.ScriptType) + + log := logger.Get() + log.Warn().Str("script_type", settings.ScriptType).Msg("updated script type") } return nil } + +// Convert array of script type features split by comma to a ScriptType struct. +func convertScriptType(scriptType string) ScriptType { + features := strings.Split(scriptType, ",") + + types := ScriptType{} + for _, feature := range features { + switch feature { + case "default": + types.Default = true + case "tagged-events": + types.TaggedEvent = true + } + } + + return types +} diff --git a/core/services/users.go b/core/services/users.go index 81cd9d16..f86f1f2c 100644 --- a/core/services/users.go +++ b/core/services/users.go @@ -2,6 +2,7 @@ package services import ( "context" + "strings" "github.com/go-faster/errors" "github.com/medama-io/medama/api" @@ -32,11 +33,17 @@ func (h *Handler) GetUser(ctx context.Context, params api.GetUserParams) (api.Ge return nil, errors.Wrap(err, "services") } + // Convert user settings to API format. + scriptFeatures := []api.UserSettingsScriptTypeItem{} + for _, v := range strings.Split(user.Settings.ScriptType, ",") { + scriptFeatures = append(scriptFeatures, api.UserSettingsScriptTypeItem(v)) + } + return &api.UserGet{ Username: user.Username, Settings: api.UserSettings{ Language: api.NewOptUserSettingsLanguage(api.UserSettingsLanguage(user.Settings.Language)), - ScriptType: api.NewOptUserSettingsScriptType(api.UserSettingsScriptType(user.Settings.ScriptType)), + ScriptType: scriptFeatures, }, DateCreated: user.DateCreated, DateUpdated: user.DateUpdated, @@ -164,8 +171,14 @@ func (h *Handler) PatchUser(ctx context.Context, req *api.UserPatch, params api. if req.Settings.Value.Language.IsSet() { settings.Language = string(req.Settings.Value.Language.Value) } - if req.Settings.Value.ScriptType.IsSet() { - settings.ScriptType = string(req.Settings.Value.ScriptType.Value) + + if req.Settings.Value.ScriptType != nil { + // Convert to string slice. + var features []string + for _, v := range req.Settings.Value.ScriptType { + features = append(features, string(v)) + } + settings.ScriptType = strings.Join(features, ",") } err = h.db.UpdateSettings(ctx, user.ID, settings) @@ -174,19 +187,25 @@ func (h *Handler) PatchUser(ctx context.Context, req *api.UserPatch, params api. return nil, errors.Wrap(err, "services") } - // Also update live runtime config. - err = h.runtimeConfig.UpdateConfig(ctx, h.db, settings) + // Also update live runtime config to dynamically update script type. + err = h.RuntimeConfig.UpdateConfig(ctx, h.db, settings) if err != nil { log.Error().Err(err).Msg("failed to update runtime config") return nil, errors.Wrap(err, "services") } } + // Convert user settings to API format. + scriptFeatures := []api.UserSettingsScriptTypeItem{} + for _, v := range strings.Split(user.Settings.ScriptType, ",") { + scriptFeatures = append(scriptFeatures, api.UserSettingsScriptTypeItem(v)) + } + return &api.UserGet{ Username: user.Username, Settings: api.UserSettings{ Language: api.NewOptUserSettingsLanguage(api.UserSettingsLanguage(user.Settings.Language)), - ScriptType: api.NewOptUserSettingsScriptType(api.UserSettingsScriptType(user.Settings.ScriptType)), + ScriptType: scriptFeatures, }, DateCreated: user.DateCreated, DateUpdated: user.DateUpdated, diff --git a/dashboard/app/api/types.d.ts b/dashboard/app/api/types.d.ts index eb58e5d0..6c820f60 100644 --- a/dashboard/app/api/types.d.ts +++ b/dashboard/app/api/types.d.ts @@ -489,7 +489,7 @@ export interface components { g: string; /** @description Custom event properties. */ d: { - [key: string]: (string | number | boolean) | undefined; + [key: string]: string | number | boolean; }; /** * @description discriminator enum property added by openapi-typescript @@ -534,11 +534,7 @@ export interface components { * @enum {string} */ language?: "en"; - /** - * @default default - * @enum {string} - */ - script_type?: "default" | "tagged-events"; + script_type?: ("default" | "tagged-events")[]; }; /** * UserGet diff --git a/dashboard/app/components/settings/Sidebar.module.css b/dashboard/app/components/settings/Sidebar.module.css index 49360835..a17da22d 100644 --- a/dashboard/app/components/settings/Sidebar.module.css +++ b/dashboard/app/components/settings/Sidebar.module.css @@ -6,6 +6,7 @@ padding: 8px; gap: 8px; + color: var(--text-dark); background-color: var(--bg-light); border-radius: 8px; @@ -16,6 +17,7 @@ border-radius: 8px; + color: var(--text-dark); font-size: 14px; text-decoration: none; diff --git a/dashboard/app/routes/settings.tracker.tsx b/dashboard/app/routes/settings.tracker.tsx index f6bbe3c2..6bd5a81d 100644 --- a/dashboard/app/routes/settings.tracker.tsx +++ b/dashboard/app/routes/settings.tracker.tsx @@ -1,4 +1,4 @@ -import { useForm } from '@mantine/form'; +import { type TransformedValues, useForm } from '@mantine/form'; import { notifications } from '@mantine/notifications'; import { type ClientActionFunctionArgs, @@ -8,6 +8,7 @@ import { useSubmit, } from '@remix-run/react'; import { valibotResolver } from 'mantine-form-valibot-resolver'; +import { useState } from 'react'; import * as v from 'valibot'; import type { components } from '@/api/types'; @@ -21,7 +22,7 @@ import { SectionTitle, SectionWrapper, } from '@/components/settings/Section'; -import { getType } from '@/utils/form'; +import { getString, getType } from '@/utils/form'; interface LoaderData { user: components['schemas']['UserGet']; @@ -59,6 +60,7 @@ export const clientLoader = async () => { export const clientAction = async ({ request }: ClientActionFunctionArgs) => { const body = await request.formData(); const type = getType(body); + const scriptType = getString(body, 'script_type'); let res: Response | undefined; switch (type) { @@ -66,7 +68,9 @@ export const clientAction = async ({ request }: ClientActionFunctionArgs) => { const update = await userUpdate({ body: { settings: { - script_type: 'default', + script_type: scriptType?.split( + ',', + ) as components['schemas']['UserGet']['settings']['script_type'], }, }, noThrow: true, @@ -104,6 +108,10 @@ export default function Index() { return; } + const [taggedEvents, setTaggedEvents] = useState( + Boolean(user.settings.script_type?.includes('tagged-events')), + ); + const code = location.hostname === 'localhost' ? getTrackingScript('[your-analytics-server].com') @@ -114,14 +122,29 @@ export default function Index() { initialValues: { _setting: 'tracker', script_type: { - default: user.settings.script_type === 'default', - 'tagged-events': user.settings.script_type === 'tagged-events', + default: true, + 'tagged-events': taggedEvents, }, }, validate: valibotResolver(trackerSchema), + transformValues: (values) => { + // It's difficult to get Radix checkboxes to work with @mantine/form for now + values.script_type['tagged-events'] = taggedEvents; + + // Convert object to comma-separated string + const scriptType = Object.entries(values.script_type) + .filter(([, value]) => value) + .map(([key]) => key) + .join(','); + + return { + ...values, + script_type: scriptType, + }; + }, }); - const handleSubmit = (values: typeof form.values) => { + const handleSubmit = (values: TransformedValues) => { submit(values, { method: 'POST' }); }; @@ -172,6 +195,8 @@ export default function Index() {

} + checked={taggedEvents} + onCheckedChange={() => setTaggedEvents(!taggedEvents)} key={form.key('script_type.tagged-events')} {...form.getInputProps('script_type.tagged-events', { type: 'checkbox', diff --git a/tracker/Taskfile.yaml b/tracker/Taskfile.yaml index 7ff6b454..927e8ec7 100644 --- a/tracker/Taskfile.yaml +++ b/tracker/Taskfile.yaml @@ -19,13 +19,13 @@ tasks: embed: deps: [build:default] cmds: - - mkdir -p ../core/client - - cp ./dist/default.min.js ../core/client/script.js # TODO: Rename to default.js - - cp ./dist/tagged-events.min.js ../core/client/tagged-events.js + - mkdir -p ../core/client/scripts + - cp ./dist/default.min.js ../core/client/scripts/default.js + - cp ./dist/tagged-events.min.js ../core/client/scripts/tagged-events.js sources: - ./dist/*.min.js generates: - - ../core/client/script.js + - ../core/client/scripts/*.js test: deps: [build:default]