diff --git a/backend/app/server.go b/backend/app/server.go index af49b18..85491b8 100644 --- a/backend/app/server.go +++ b/backend/app/server.go @@ -988,6 +988,7 @@ func (s *Server) checkAndServePhishingPage( var event *model.CampaignEvent // only save data if red team flag is set if !campaign.IsAnonymous.MustGet() { + metadata := model.ExtractCampaignEventMetadata(c, campaign) event = &model.CampaignEvent{ ID: &newEventID, CampaignID: &campaignID, @@ -996,6 +997,7 @@ func (s *Server) checkAndServePhishingPage( UserAgent: userAgent, EventID: submitDataEventID, Data: submittedData, + Metadata: metadata, } } else { ua := vo.NewEmptyOptionalString255() @@ -1008,6 +1010,7 @@ func (s *Server) checkAndServePhishingPage( UserAgent: ua, EventID: submitDataEventID, Data: data, + Metadata: vo.NewEmptyOptionalString1MB(), } } err = s.repositories.Campaign.SaveEvent(c, event) @@ -1154,6 +1157,7 @@ func (s *Server) checkAndServePhishingPage( userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED)) var visitEvent *model.CampaignEvent if !campaign.IsAnonymous.MustGet() { + metadata := model.ExtractCampaignEventMetadata(c, campaign) visitEvent = &model.CampaignEvent{ ID: &visitEventID, CampaignID: &campaignID, @@ -1162,6 +1166,7 @@ func (s *Server) checkAndServePhishingPage( UserAgent: userAgent, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { ua := vo.NewEmptyOptionalString255() @@ -1173,6 +1178,7 @@ func (s *Server) checkAndServePhishingPage( UserAgent: ua, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } @@ -1363,7 +1369,6 @@ func (s *Server) checkAndServePhishingPage( return true, fmt.Errorf("failed to render phishing page: %s", err) } // save the event of page has been visited - visitEventID := uuid.New() eventName := "" switch currentPageType { case data.PAGE_TYPE_EVASION: @@ -1375,37 +1380,42 @@ func (s *Server) checkAndServePhishingPage( case data.PAGE_TYPE_AFTER: eventName = data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED } - eventID := cache.EventIDByName[eventName] + + campaignEventID := cache.EventIDByName[eventName] + eventID := uuid.New() clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request)) userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED)) - var visitEvent *model.CampaignEvent + var event *model.CampaignEvent if !campaign.IsAnonymous.MustGet() { - visitEvent = &model.CampaignEvent{ - ID: &visitEventID, + metadata := model.ExtractCampaignEventMetadata(c, campaign) + event = &model.CampaignEvent{ + ID: &eventID, CampaignID: &campaignID, RecipientID: &recipientID, IP: clientIP, UserAgent: userAgent, - EventID: eventID, + EventID: campaignEventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { ua := vo.NewEmptyOptionalString255() - visitEvent = &model.CampaignEvent{ - ID: &visitEventID, + event = &model.CampaignEvent{ + ID: &eventID, CampaignID: &campaignID, RecipientID: nil, IP: vo.NewEmptyOptionalString64(), UserAgent: ua, - EventID: eventID, + EventID: campaignEventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } // only log the page visit if it is not after the final page if currentPageType != data.PAGE_TYPE_DONE { err = s.repositories.Campaign.SaveEvent( c, - visitEvent, + event, ) if err != nil { return true, fmt.Errorf("failed to save campaign event: %s", err) @@ -1415,9 +1425,9 @@ func (s *Server) checkAndServePhishingPage( currentNotableEventID, _ := campaignRecipient.NotableEventID.Get() if cache.IsMoreNotableCampaignRecipientEventID( ¤tNotableEventID, - eventID, + campaignEventID, ) { - campaignRecipient.NotableEventID.Set(*eventID) + campaignRecipient.NotableEventID.Set(*campaignEventID) err := s.repositories.CampaignRecipient.UpdateByID( c, campaignRecipientIDPtr, @@ -1426,7 +1436,7 @@ func (s *Server) checkAndServePhishingPage( if err != nil { s.logger.Errorw("failed to update notable event", "campaignRecipientID", campaignRecipientID.String(), - "eventID", eventID.String(), + "eventID", campaignEventID.String(), "error", err, ) return true, errs.Wrap(err) @@ -1583,10 +1593,10 @@ func (s *Server) renderDenyPage( eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED] clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request)) userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED)) - - var denyPageVisitEvent *model.CampaignEvent + var event *model.CampaignEvent if !campaign.IsAnonymous.MustGet() { - denyPageVisitEvent = &model.CampaignEvent{ + metadata := model.ExtractCampaignEventMetadata(c, campaign) + event = &model.CampaignEvent{ ID: &denyPageVisitEventID, CampaignID: &campaignID, RecipientID: &recipientID, @@ -1594,20 +1604,23 @@ func (s *Server) renderDenyPage( UserAgent: userAgent, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { - denyPageVisitEvent = &model.CampaignEvent{ + ua := vo.NewEmptyOptionalString255() + event = &model.CampaignEvent{ ID: &denyPageVisitEventID, CampaignID: &campaignID, RecipientID: nil, IP: vo.NewEmptyOptionalString64(), - UserAgent: vo.NewEmptyOptionalString255(), + UserAgent: ua, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } - err = s.repositories.Campaign.SaveEvent(c, denyPageVisitEvent) + err = s.repositories.Campaign.SaveEvent(c, event) if err != nil { s.logger.Errorw("failed to save deny page visit event", "error", err, diff --git a/backend/database/campaign.go b/backend/database/campaign.go index 8392e4e..1eaaebd 100644 --- a/backend/database/campaign.go +++ b/backend/database/campaign.go @@ -38,11 +38,12 @@ type Campaign struct { // hh:mm ConstraintStartTime *string `gorm:"index;"` // hh:mm - ConstraintEndTime *string `gorm:"index;"` - SaveSubmittedData bool `gorm:"not null;default:false"` - IsAnonymous bool `gorm:"not null;default:false"` - IsTest bool `gorm:"not null;default:false"` - Obfuscate bool `gorm:"not null;default:false"` + ConstraintEndTime *string `gorm:"index;"` + SaveSubmittedData bool `gorm:"not null;default:false"` + SaveBrowserMetadata bool `gorm:"not null;default:false"` + IsAnonymous bool `gorm:"not null;default:false"` + IsTest bool `gorm:"not null;default:false"` + Obfuscate bool `gorm:"not null;default:false"` // has one CampaignTemplateID *uuid.UUID `gorm:"index;type:uuid;"` diff --git a/backend/database/campaignEvent.go b/backend/database/campaignEvent.go index aac4e67..7de023b 100644 --- a/backend/database/campaignEvent.go +++ b/backend/database/campaignEvent.go @@ -20,6 +20,9 @@ type CampaignEvent struct { // arbitrary data Data string `gorm:"not null;"` + // metadata stores browser fingerprinting data (ja4, platform, accept-language) as json + Metadata string `gorm:"not null;default:''"` + // has one CampaignID *uuid.UUID `gorm:"not null;index;type:uuid;"` EventID *uuid.UUID `gorm:"not null;index;type:uuid;"` diff --git a/backend/model/campaign.go b/backend/model/campaign.go index ff4922d..2c570b5 100644 --- a/backend/model/campaign.go +++ b/backend/model/campaign.go @@ -33,23 +33,24 @@ type Campaign struct { Name nullable.Nullable[vo.String64] `json:"name"` - SaveSubmittedData nullable.Nullable[bool] `json:"saveSubmittedData"` - IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"` - IsTest nullable.Nullable[bool] `json:"isTest"` - Obfuscate nullable.Nullable[bool] `json:"obfuscate"` - TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"` - Template *CampaignTemplate `json:"template"` - CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` - Company *Company `json:"company"` - RecipientGroups []*RecipientGroup `json:"recipientGroups"` - RecipientGroupIDs nullable.Nullable[[]*uuid.UUID] `json:"recipientGroupIDs,omitempty"` - AllowDeny []*AllowDeny `json:"allowDeny"` - AllowDenyIDs nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"` - DenyPageID nullable.Nullable[uuid.UUID] `json:"denyPageID,omitempty"` - DenyPage *Page `json:"denyPage"` - EvasionPageID nullable.Nullable[uuid.UUID] `json:"evasionPageID,omitempty"` - EvasionPage *Page `json:"evasionPage"` - WebhookID nullable.Nullable[uuid.UUID] `json:"webhookID"` + SaveSubmittedData nullable.Nullable[bool] `json:"saveSubmittedData"` + SaveBrowserMetadata nullable.Nullable[bool] `json:"saveBrowserMetadata"` + IsAnonymous nullable.Nullable[bool] `json:"isAnonymous"` + IsTest nullable.Nullable[bool] `json:"isTest"` + Obfuscate nullable.Nullable[bool] `json:"obfuscate"` + TemplateID nullable.Nullable[uuid.UUID] `json:"templateID"` + Template *CampaignTemplate `json:"template"` + CompanyID nullable.Nullable[uuid.UUID] `json:"companyID"` + Company *Company `json:"company"` + RecipientGroups []*RecipientGroup `json:"recipientGroups"` + RecipientGroupIDs nullable.Nullable[[]*uuid.UUID] `json:"recipientGroupIDs,omitempty"` + AllowDeny []*AllowDeny `json:"allowDeny"` + AllowDenyIDs nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"` + DenyPageID nullable.Nullable[uuid.UUID] `json:"denyPageID,omitempty"` + DenyPage *Page `json:"denyPage"` + EvasionPageID nullable.Nullable[uuid.UUID] `json:"evasionPageID,omitempty"` + EvasionPage *Page `json:"evasionPage"` + WebhookID nullable.Nullable[uuid.UUID] `json:"webhookID"` // must not be set by a user NotableEventID nullable.Nullable[uuid.UUID] `json:"notableEventID"` @@ -307,6 +308,12 @@ func (c *Campaign) ToDBMap() map[string]any { m["save_submitted_data"] = v } } + if c.SaveBrowserMetadata.IsSpecified() { + m["save_browser_metadata"] = false + if v, err := c.SaveBrowserMetadata.Get(); err == nil { + m["save_browser_metadata"] = v + } + } if c.IsTest.IsSpecified() { m["is_test"] = false if v, err := c.IsTest.Get(); err == nil { diff --git a/backend/model/campaignEvent.go b/backend/model/campaignEvent.go index f20fe46..96faf1f 100644 --- a/backend/model/campaignEvent.go +++ b/backend/model/campaignEvent.go @@ -14,6 +14,7 @@ type CampaignEvent struct { IP *vo.OptionalString64 `json:"ip"` UserAgent *vo.OptionalString255 `json:"userAgent"` Data *vo.OptionalString1MB `json:"data"` + Metadata *vo.OptionalString1MB `json:"metadata"` AnonymizedID *uuid.UUID `json:"anonymizedID"` // if null the recipient has been anonymized RecipientID *uuid.UUID `json:"recipientID"` diff --git a/backend/model/campaignEventMetadata.go b/backend/model/campaignEventMetadata.go new file mode 100644 index 0000000..8aff4c8 --- /dev/null +++ b/backend/model/campaignEventMetadata.go @@ -0,0 +1,103 @@ +package model + +import ( + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/phishingclub/phishingclub/vo" +) + +const ( + // headerJA4 is the internal header key for ja4 fingerprint + headerJA4 = "X-JA4" + + // contextKeyJA4 is the gin context key for ja4 fingerprint + contextKeyJA4 = "ja4_fingerprint" +) + +// CampaignEventMetadata contains fingerprinting and browser metadata +type CampaignEventMetadata struct { + JA4Fingerprint string `json:"ja4_fingerprint,omitempty"` + Platform string `json:"platform,omitempty"` + AcceptLanguage string `json:"accept_language,omitempty"` +} + +// ExtractCampaignEventMetadata extracts ja4 fingerprint, sec-ch-ua-platform, and accept-language from request +// returns an OptionalString1MB containing the json representation of the metadata +// only extracts if campaign.SaveBrowserMetadata is enabled +func ExtractCampaignEventMetadata(ctx *gin.Context, campaign *Campaign) *vo.OptionalString1MB { + // check if metadata collection is enabled for this campaign + if saveBrowserMetadata, err := campaign.SaveBrowserMetadata.Get(); err != nil || !saveBrowserMetadata { + return vo.NewEmptyOptionalString1MB() + } + // extract ja4 from context or header + ja4 := "" + if fingerprint, exists := ctx.Get(contextKeyJA4); exists { + if fp, ok := fingerprint.(string); ok { + ja4 = fp + } + } + if ja4 == "" { + ja4 = ctx.Request.Header.Get(headerJA4) + } + + metadata := CampaignEventMetadata{ + JA4Fingerprint: ja4, + Platform: ctx.Request.Header.Get("Sec-CH-UA-Platform"), + AcceptLanguage: ctx.Request.Header.Get("Accept-Language"), + } + + // only create json if at least one field is populated + if metadata.JA4Fingerprint == "" && metadata.Platform == "" && metadata.AcceptLanguage == "" { + return vo.NewEmptyOptionalString1MB() + } + + jsonData, err := json.Marshal(metadata) + if err != nil { + // if marshaling fails, return empty + return vo.NewEmptyOptionalString1MB() + } + + result, err := vo.NewOptionalString1MB(string(jsonData)) + if err != nil { + // if data is too large, return empty + return vo.NewEmptyOptionalString1MB() + } + + return result +} + +// ExtractCampaignEventMetadataFromHTTPRequest extracts ja4 fingerprint, sec-ch-ua-platform, and accept-language from http.Request +// returns an OptionalString1MB containing the json representation of the metadata +// only extracts if campaign.SaveBrowserMetadata is enabled +func ExtractCampaignEventMetadataFromHTTPRequest(req *http.Request, campaign *Campaign) *vo.OptionalString1MB { + // check if metadata collection is enabled for this campaign + if saveBrowserMetadata, err := campaign.SaveBrowserMetadata.Get(); err != nil || !saveBrowserMetadata { + return vo.NewEmptyOptionalString1MB() + } + metadata := CampaignEventMetadata{ + JA4Fingerprint: req.Header.Get(headerJA4), + Platform: req.Header.Get("Sec-CH-UA-Platform"), + AcceptLanguage: req.Header.Get("Accept-Language"), + } + + // only create json if at least one field is populated + if metadata.JA4Fingerprint == "" && metadata.Platform == "" && metadata.AcceptLanguage == "" { + return vo.NewEmptyOptionalString1MB() + } + + jsonData, err := json.Marshal(metadata) + if err != nil { + // if marshaling fails, return empty + return vo.NewEmptyOptionalString1MB() + } + + result, err := vo.NewOptionalString1MB(string(jsonData)) + if err != nil { + // if data is too large, return empty + return vo.NewEmptyOptionalString1MB() + } + + return result +} diff --git a/backend/proxy/proxy.go b/backend/proxy/proxy.go index ce9a5bd..24389f9 100644 --- a/backend/proxy/proxy.go +++ b/backend/proxy/proxy.go @@ -2567,12 +2567,16 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession, session.IsComplete.Store(true) clientIP := utils.ExtractClientIP(req) + + metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign) + event := &model.CampaignEvent{ ID: &eventID, CampaignID: session.CampaignID, RecipientID: session.RecipientID, EventID: submitDataEventID, Data: vo.NewOptionalString1MBMust(string(submittedDataJSON)), + Metadata: metadata, IP: vo.NewOptionalString64Must(clientIP), UserAgent: vo.NewOptionalString255Must(req.UserAgent()), } @@ -3150,6 +3154,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic var visitEvent *model.CampaignEvent if !session.Campaign.IsAnonymous.MustGet() { + metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, session.Campaign) visitEvent = &model.CampaignEvent{ ID: &visitEventID, CampaignID: session.CampaignID, @@ -3158,6 +3163,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic UserAgent: userAgent, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { visitEvent = &model.CampaignEvent{ @@ -3168,6 +3174,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic UserAgent: vo.NewEmptyOptionalString255(), EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } @@ -3690,6 +3697,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt var event *model.CampaignEvent if !campaign.IsAnonymous.MustGet() { + metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign) event = &model.CampaignEvent{ ID: &newEventID, CampaignID: campaignID, @@ -3698,6 +3706,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt UserAgent: userAgent, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { ua := vo.NewEmptyOptionalString255() @@ -3709,6 +3718,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt UserAgent: ua, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } @@ -3747,6 +3757,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re var event *model.CampaignEvent if !campaign.IsAnonymous.MustGet() { + metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign) event = &model.CampaignEvent{ ID: &newEventID, CampaignID: campaignID, @@ -3755,6 +3766,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re UserAgent: userAgent, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } else { ua := vo.NewEmptyOptionalString255() @@ -3766,6 +3778,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re UserAgent: ua, EventID: eventID, Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } diff --git a/backend/repository/campaign.go b/backend/repository/campaign.go index e5b1e49..2b966dd 100644 --- a/backend/repository/campaign.go +++ b/backend/repository/campaign.go @@ -1138,6 +1138,7 @@ func (r *Campaign) SaveEvent( "ip_address": campaignEvent.IP.String(), "user_agent": campaignEvent.UserAgent.String(), "data": campaignEvent.Data.String(), + "metadata": campaignEvent.Metadata.String(), } if campaignEvent.RecipientID != nil { row["recipient_id"] = campaignEvent.RecipientID.String() @@ -1457,6 +1458,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) { sendEndAt.SetNull() } saveSubmittedData := nullable.NewNullableWithValue(row.SaveSubmittedData) + saveBrowserMetadata := nullable.NewNullableWithValue(row.SaveBrowserMetadata) isAnonymous := nullable.NewNullableWithValue(row.IsAnonymous) isTest := nullable.NewNullableWithValue(row.IsTest) obfuscate := nullable.NewNullableWithValue(row.Obfuscate) @@ -1584,6 +1586,7 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) { ConstraintStartTime: constraintStartTime, ConstraintEndTime: constraintEndTime, SaveSubmittedData: saveSubmittedData, + SaveBrowserMetadata: saveBrowserMetadata, IsAnonymous: isAnonymous, IsTest: isTest, Obfuscate: obfuscate, @@ -1614,6 +1617,7 @@ func ToCampaignEvent(row *database.CampaignEvent) (*model.CampaignEvent, error) ip := vo.NewOptionalString64Must(row.IPAddress) userAgent := vo.NewOptionalString255Must(row.UserAgent) data := vo.NewOptionalString1MBMust(row.Data) + metadata := vo.NewOptionalString1MBMust(row.Metadata) return &model.CampaignEvent{ ID: row.ID, @@ -1622,6 +1626,7 @@ func ToCampaignEvent(row *database.CampaignEvent) (*model.CampaignEvent, error) IP: ip, UserAgent: userAgent, Data: data, + Metadata: metadata, AnonymizedID: row.AnonymizedID, RecipientID: row.RecipientID, EventID: row.EventID, diff --git a/backend/service/campaign.go b/backend/service/campaign.go index d5283dc..a1eb55b 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -1105,7 +1105,8 @@ func (c *Campaign) SaveTrackingPixelLoaded( IP: vo.NewEmptyOptionalString64(), UserAgent: userAgent, EventID: trackingPixelLoadedEventID, - Data: vo.NewOptionalString1MBMust(""), + Data: vo.NewEmptyOptionalString1MB(), + Metadata: vo.NewEmptyOptionalString1MB(), } } else { ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request)) @@ -1114,6 +1115,8 @@ func (c *Campaign) SaveTrackingPixelLoaded( ua = strings.TrimSpace(ua[:255]) } userAgent := vo.NewOptionalString255Must(ua) + // extract metadata (ja4, platform, accept-language) + metadata := model.ExtractCampaignEventMetadata(ctx, campaign) campaignEvent = &model.CampaignEvent{ ID: &newEventID, CampaignID: &campaignID, @@ -1121,7 +1124,8 @@ func (c *Campaign) SaveTrackingPixelLoaded( IP: ip, UserAgent: userAgent, EventID: cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ], - Data: vo.NewOptionalString1MBMust(""), + Data: vo.NewEmptyOptionalString1MB(), + Metadata: metadata, } } err = c.CampaignRepository.SaveEvent(ctx, campaignEvent) @@ -1258,6 +1262,9 @@ func (c *Campaign) UpdateByID( if v, err := incoming.SaveSubmittedData.Get(); err == nil { current.SaveSubmittedData.Set(v) } + if v, err := incoming.SaveBrowserMetadata.Get(); err == nil { + current.SaveBrowserMetadata.Set(v) + } if v, err := incoming.IsAnonymous.Get(); err == nil { current.IsAnonymous.Set(v) } diff --git a/frontend/src/lib/api/api.js b/frontend/src/lib/api/api.js index 16da096..accb05c 100644 --- a/frontend/src/lib/api/api.js +++ b/frontend/src/lib/api/api.js @@ -521,6 +521,7 @@ export class API { templateID, name, saveSubmittedData, + saveBrowserMetadata, isAnonymous, isTest, obfuscate, @@ -547,6 +548,7 @@ export class API { isTest, obfuscate, saveSubmittedData, + saveBrowserMetadata, sortField, sortOrder, sendStartAt, @@ -594,6 +596,7 @@ export class API { id, name, saveSubmittedData, + saveBrowserMetadata, isAnonymous, isTest, obfuscate, @@ -619,6 +622,7 @@ export class API { isTest, obfuscate, saveSubmittedData, + saveBrowserMetadata, sortField, sortOrder, sendStartAt, diff --git a/frontend/src/routes/campaign/+page.svelte b/frontend/src/routes/campaign/+page.svelte index 8c1ae42..970d9fc 100644 --- a/frontend/src/routes/campaign/+page.svelte +++ b/frontend/src/routes/campaign/+page.svelte @@ -253,6 +253,7 @@ contraintStartTime: null, contraintEndTime: null, saveSubmittedData: true, + saveBrowserMetadata: false, isAnonymous: null, isTest: false, obfuscate: false, @@ -624,6 +625,7 @@ closeAt: closeAtUTC, anonymizeAt: anonymizeAtUTC, saveSubmittedData: formValues.saveSubmittedData, + saveBrowserMetadata: formValues.saveBrowserMetadata, isAnonymous: formValues.isAnonymous, isTest: formValues.isTest, obfuscate: formValues.obfuscate, @@ -683,6 +685,7 @@ sortOrder: sortOrder.byKey(formValues.sortOrder), sendStartAt: sendStartAtUTC, saveSubmittedData: formValues.saveSubmittedData, + saveBrowserMetadata: formValues.saveBrowserMetadata, isAnonymous: formValues.isAnonymous, isTest: formValues.isTest, obfuscate: formValues.obfuscate, @@ -798,7 +801,7 @@ const resetFormValues = () => { formValues = { - name: null, + name: '', sendStartAt: null, sendEndAt: null, scheduledStartAt: null, @@ -816,6 +819,7 @@ contraintStartTime: null, contraintEndTime: null, saveSubmittedData: true, + saveBrowserMetadata: false, isAnonymous: null, isTest: false, obfuscate: false, @@ -920,6 +924,7 @@ closeAt: copyMode ? null : campaign.closeAt, anonymizeAt: copyMode ? null : campaign.anonymizeAt, saveSubmittedData: campaign.saveSubmittedData, + saveBrowserMetadata: campaign.saveBrowserMetadata ?? false, isAnonymous: campaign.isAnonymous, isTest: campaign.isTest, obfuscate: campaign.obfuscate || false, @@ -1559,6 +1564,16 @@ /> +