ai cooking
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

okay session id here we go (#380)

* okay session id here we go

* just context baby

---------

Co-authored-by: paul miller <paul.miller>

authored by

Paul Miller
paul miller
and committed by
GitHub
ad72c271 4820cad8

+367 -34
+46 -2
cmd/careme/middleware.go
··· 20 20 "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts" 21 21 ) 22 22 23 + const ( 24 + sessionCookieName = "careme_session_id" 25 + sessionCookieMaxAge = 30 * 60 26 + ) 27 + 23 28 type logger struct { 24 29 http.Handler 25 30 } ··· 68 73 69 74 func (t *appInsightsTelemetryTracker) TrackRequest(ctx context.Context, method, url string, duration time.Duration, responseCode string) { 70 75 request := azureappinsights.NewRequestTelemetry(method, url, duration, responseCode) 76 + tags := contracts.ContextTags(request.ContextTags()) 71 77 if operationID, ok := logsetup.OperationIDFromContext(ctx); ok { 72 - contracts.ContextTags(request.ContextTags()).Operation().SetId(operationID) 78 + tags.Operation().SetId(operationID) 79 + } 80 + if sessionID, ok := logsetup.SessionIDFromContext(ctx); ok { 81 + tags.Session().SetId(sessionID) 73 82 } 74 83 t.client.Track(request) 75 84 } ··· 188 197 h.Handler.ServeHTTP(w, r.WithContext(ctx)) 189 198 } 190 199 200 + type sessionIDHandler struct { 201 + http.Handler 202 + } 203 + 204 + func (h *sessionIDHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 205 + sessionID := readOrCreateSessionID(r) 206 + ctx := logsetup.WithSessionID(r.Context(), sessionID) 207 + http.SetCookie(w, sessionCookie(r, sessionID)) 208 + h.Handler.ServeHTTP(w, r.WithContext(ctx)) 209 + } 210 + 211 + func readOrCreateSessionID(r *http.Request) string { 212 + cookie, err := r.Cookie(sessionCookieName) 213 + if err != nil || cookie.Value == "" { 214 + return uuid.NewString() 215 + } 216 + if _, err := uuid.Parse(cookie.Value); err != nil { 217 + return uuid.NewString() 218 + } 219 + return cookie.Value 220 + } 221 + 222 + func sessionCookie(r *http.Request, sessionID string) *http.Cookie { 223 + return &http.Cookie{ 224 + Name: sessionCookieName, 225 + Value: sessionID, 226 + Path: "/", 227 + MaxAge: sessionCookieMaxAge, 228 + HttpOnly: true, 229 + SameSite: http.SameSiteLaxMode, 230 + Secure: r.TLS != nil, 231 + } 232 + } 233 + 191 234 func WithMiddleware(h http.Handler) http.Handler { 192 235 h = &recoverer{h} 193 236 h = newAppInsightsTrackerFromEnv(h) 194 237 h = &logger{h} 195 - return &operationIDHandler{h} 238 + h = &operationIDHandler{h} 239 + return &sessionIDHandler{h} 196 240 }
+153
cmd/careme/middleware_test.go
··· 17 17 duration time.Duration 18 18 responseCode string 19 19 operationID string 20 + sessionID string 20 21 } 21 22 22 23 type fakeRequestTracker struct { ··· 25 26 26 27 func (f *fakeRequestTracker) TrackRequest(ctx context.Context, method, url string, duration time.Duration, responseCode string) { 27 28 operationID, _ := logsetup.OperationIDFromContext(ctx) 29 + sessionID, _ := logsetup.SessionIDFromContext(ctx) 28 30 f.calls = append(f.calls, trackedRequest{ 29 31 method: method, 30 32 url: url, 31 33 duration: duration, 32 34 responseCode: responseCode, 33 35 operationID: operationID, 36 + sessionID: sessionID, 34 37 }) 35 38 } 36 39 ··· 151 154 if tracker.calls[0].operationID != "op-555" { 152 155 t.Fatalf("expected tracker to receive operation id op-555, got %q", tracker.calls[0].operationID) 153 156 } 157 + } 158 + 159 + func TestAppInsightsTrackerIncludesSessionIDFromContext(t *testing.T) { 160 + tracker := &fakeRequestTracker{} 161 + mw := &appInsightsTracker{ 162 + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 163 + w.WriteHeader(http.StatusAccepted) 164 + }), 165 + tracker: tracker, 166 + } 167 + 168 + req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 169 + req = req.WithContext(logsetup.WithSessionID(req.Context(), "sess-555")) 170 + rec := httptest.NewRecorder() 171 + mw.ServeHTTP(rec, req) 172 + 173 + if len(tracker.calls) != 1 { 174 + t.Fatalf("expected 1 tracked request, got %d", len(tracker.calls)) 175 + } 176 + if tracker.calls[0].sessionID != "sess-555" { 177 + t.Fatalf("expected tracker to receive session id sess-555, got %q", tracker.calls[0].sessionID) 178 + } 179 + } 180 + 181 + func TestSessionIDHandlerIssuesCookieWhenMissing(t *testing.T) { 182 + var gotSessionID string 183 + handler := &sessionIDHandler{ 184 + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 185 + var ok bool 186 + gotSessionID, ok = logsetup.SessionIDFromContext(r.Context()) 187 + if !ok { 188 + t.Fatal("expected session id in context") 189 + } 190 + w.WriteHeader(http.StatusNoContent) 191 + }), 192 + } 193 + 194 + req := httptest.NewRequest(http.MethodGet, "http://careme.cooking/about", nil) 195 + rec := httptest.NewRecorder() 196 + handler.ServeHTTP(rec, req) 197 + 198 + cookie := rec.Result().Cookies() 199 + if len(cookie) != 1 { 200 + t.Fatalf("expected 1 cookie, got %d", len(cookie)) 201 + } 202 + if cookie[0].Name != sessionCookieName { 203 + t.Fatalf("expected cookie %q, got %q", sessionCookieName, cookie[0].Name) 204 + } 205 + if cookie[0].Value == "" { 206 + t.Fatal("expected non-empty session cookie value") 207 + } 208 + if gotSessionID != cookie[0].Value { 209 + t.Fatalf("expected context session id %q, got %q", cookie[0].Value, gotSessionID) 210 + } 211 + if cookie[0].MaxAge != sessionCookieMaxAge { 212 + t.Fatalf("expected MaxAge %d, got %d", sessionCookieMaxAge, cookie[0].MaxAge) 213 + } 214 + if !cookie[0].HttpOnly { 215 + t.Fatal("expected cookie to be HttpOnly") 216 + } 217 + if cookie[0].SameSite != http.SameSiteLaxMode { 218 + t.Fatalf("expected SameSite Lax, got %v", cookie[0].SameSite) 219 + } 220 + if cookie[0].Secure { 221 + t.Fatal("expected non-TLS request cookie to be insecure") 222 + } 223 + } 224 + 225 + func TestSessionIDHandlerReusesValidCookie(t *testing.T) { 226 + const sessionID = "8d31449a-1f55-4e8b-8812-9d9a3c0f45d7" 227 + var gotSessionID string 228 + handler := &sessionIDHandler{ 229 + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 230 + gotSessionID, _ = logsetup.SessionIDFromContext(r.Context()) 231 + w.WriteHeader(http.StatusNoContent) 232 + }), 233 + } 234 + 235 + req := httptest.NewRequest(http.MethodGet, "https://careme.cooking/about", nil) 236 + req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: sessionID}) 237 + rec := httptest.NewRecorder() 238 + handler.ServeHTTP(rec, req) 239 + 240 + cookie := findCookie(t, rec.Result().Cookies(), sessionCookieName) 241 + if cookie.Value != sessionID { 242 + t.Fatalf("expected session cookie %q, got %q", sessionID, cookie.Value) 243 + } 244 + if gotSessionID != sessionID { 245 + t.Fatalf("expected context session id %q, got %q", sessionID, gotSessionID) 246 + } 247 + if !cookie.Secure { 248 + t.Fatal("expected TLS request cookie to be secure") 249 + } 250 + } 251 + 252 + func TestSessionIDHandlerReplacesInvalidCookie(t *testing.T) { 253 + handler := &sessionIDHandler{ 254 + Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 255 + w.WriteHeader(http.StatusNoContent) 256 + }), 257 + } 258 + 259 + req := httptest.NewRequest(http.MethodGet, "http://careme.cooking/about", nil) 260 + req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: "not-a-uuid"}) 261 + rec := httptest.NewRecorder() 262 + handler.ServeHTTP(rec, req) 263 + 264 + cookie := findCookie(t, rec.Result().Cookies(), sessionCookieName) 265 + if cookie.Value == "not-a-uuid" { 266 + t.Fatal("expected invalid session cookie to be replaced") 267 + } 268 + } 269 + 270 + func TestWithMiddlewareProvidesBothIDs(t *testing.T) { 271 + var operationID string 272 + var sessionID string 273 + handler := WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 274 + operationID, _ = logsetup.OperationIDFromContext(r.Context()) 275 + sessionID, _ = logsetup.SessionIDFromContext(r.Context()) 276 + w.WriteHeader(http.StatusNoContent) 277 + })) 278 + 279 + req := httptest.NewRequest(http.MethodGet, "http://careme.cooking/about", nil) 280 + rec := httptest.NewRecorder() 281 + handler.ServeHTTP(rec, req) 282 + 283 + if operationID == "" { 284 + t.Fatal("expected operation id in context") 285 + } 286 + if sessionID == "" { 287 + t.Fatal("expected session id in context") 288 + } 289 + if rec.Header().Get("X-Operation-ID") != operationID { 290 + t.Fatalf("expected X-Operation-ID %q, got %q", operationID, rec.Header().Get("X-Operation-ID")) 291 + } 292 + cookie := findCookie(t, rec.Result().Cookies(), sessionCookieName) 293 + if cookie.Value != sessionID { 294 + t.Fatalf("expected session cookie %q, got %q", sessionID, cookie.Value) 295 + } 296 + } 297 + 298 + func findCookie(t *testing.T, cookies []*http.Cookie, name string) *http.Cookie { 299 + t.Helper() 300 + for _, cookie := range cookies { 301 + if cookie.Name == name { 302 + return cookie 303 + } 304 + } 305 + t.Fatalf("expected cookie %q", name) 306 + return nil 154 307 } 155 308 156 309 func TestParseAppInsightsConnectionString(t *testing.T) {
+2 -2
cmd/careme/web.go
··· 84 84 GoogleTagScript template.HTML 85 85 Style seasons.Style 86 86 }{ 87 - ClarityScript: templates.ClarityScript(), 87 + ClarityScript: templates.ClarityScript(ctx), 88 88 GoogleTagScript: templates.GoogleTagScript(), 89 89 Style: seasons.GetCurrentStyle(), 90 90 } ··· 126 126 Style seasons.Style 127 127 ServerSignedIn bool 128 128 }{ 129 - ClarityScript: templates.ClarityScript(), 129 + ClarityScript: templates.ClarityScript(ctx), 130 130 GoogleTagScript: templates.GoogleTagScript(), 131 131 User: currentUser, 132 132 FavoriteStoreName: favoriteStoreName,
+1 -1
internal/locations/locations.go
··· 356 356 Locations: locs, 357 357 Zip: zip, 358 358 FavoriteStore: favoriteStore, 359 - ClarityScript: templates.ClarityScript(), 359 + ClarityScript: templates.ClarityScript(ctx), 360 360 GoogleTagScript: templates.GoogleTagScript(), 361 361 Style: seasons.GetCurrentStyle(), 362 362 ServerSignedIn: serverSignedIn,
+1 -1
internal/locations/mock.go
··· 64 64 Locations: lo.Values(fakes), 65 65 Zip: r.URL.Query().Get("zip"), 66 66 FavoriteStore: "", 67 - ClarityScript: templates.ClarityScript(), 67 + ClarityScript: templates.ClarityScript(r.Context()), 68 68 GoogleTagScript: templates.GoogleTagScript(), 69 69 Style: seasons.GetCurrentStyle(), 70 70 }
+26 -1
internal/logsetup/context.go
··· 7 7 8 8 type contextKey string 9 9 10 - const operationIDContextKey contextKey = "operation_id" 10 + const ( 11 + operationIDContextKey contextKey = "operation_id" 12 + sessionIDContextKey contextKey = "session_id" 13 + ) 11 14 12 15 func WithOperationID(ctx context.Context, operationID string) context.Context { 13 16 if operationID == "" { ··· 27 30 return operationID, true 28 31 } 29 32 33 + func WithSessionID(ctx context.Context, sessionID string) context.Context { 34 + if sessionID == "" { 35 + return ctx 36 + } 37 + return context.WithValue(ctx, sessionIDContextKey, sessionID) 38 + } 39 + 40 + func SessionIDFromContext(ctx context.Context) (string, bool) { 41 + if ctx == nil { 42 + return "", false 43 + } 44 + sessionID, ok := ctx.Value(sessionIDContextKey).(string) 45 + if !ok || sessionID == "" { 46 + return "", false 47 + } 48 + return sessionID, true 49 + } 50 + 51 + // Cosider https://github.com/PumpkinSeed/slog-context instead 30 52 type contextHandler struct { 31 53 handler slog.Handler 32 54 } ··· 42 64 func (h *contextHandler) Handle(ctx context.Context, record slog.Record) error { 43 65 if operationID, ok := OperationIDFromContext(ctx); ok { 44 66 record.AddAttrs(slog.String("operation_id", operationID)) 67 + } 68 + if sessionID, ok := SessionIDFromContext(ctx); ok { 69 + record.AddAttrs(slog.String("session_id", sessionID)) 45 70 } 46 71 return h.handler.Handle(ctx, record) 47 72 }
+42
internal/logsetup/context_test.go
··· 21 21 } 22 22 } 23 23 24 + func TestContextHandlerAddsSessionID(t *testing.T) { 25 + var buf bytes.Buffer 26 + logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 27 + ctx := WithSessionID(context.Background(), "sess-123") 28 + 29 + logger.InfoContext(ctx, "hello") 30 + 31 + output := buf.String() 32 + if !strings.Contains(output, "session_id=sess-123") { 33 + t.Fatalf("expected session_id in output, got %q", output) 34 + } 35 + } 36 + 37 + func TestContextHandlerAddsBothIDs(t *testing.T) { 38 + var buf bytes.Buffer 39 + logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 40 + ctx := WithOperationID(context.Background(), "op-123") 41 + ctx = WithSessionID(ctx, "sess-123") 42 + 43 + logger.InfoContext(ctx, "hello") 44 + 45 + output := buf.String() 46 + if !strings.Contains(output, "operation_id=op-123") { 47 + t.Fatalf("expected operation_id in output, got %q", output) 48 + } 49 + if !strings.Contains(output, "session_id=sess-123") { 50 + t.Fatalf("expected session_id in output, got %q", output) 51 + } 52 + } 53 + 24 54 func TestContextHandlerSkipsMissingOperationID(t *testing.T) { 25 55 var buf bytes.Buffer 26 56 logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) ··· 32 62 t.Fatalf("did not expect operation_id in output, got %q", output) 33 63 } 34 64 } 65 + 66 + func TestContextHandlerSkipsMissingSessionID(t *testing.T) { 67 + var buf bytes.Buffer 68 + logger := slog.New(newContextHandler(slog.NewTextHandler(&buf, nil))) 69 + 70 + logger.InfoContext(context.Background(), "hello") 71 + 72 + output := buf.String() 73 + if strings.Contains(output, "session_id=") { 74 + t.Fatalf("did not expect session_id in output, got %q", output) 75 + } 76 + }
+2 -2
internal/recipes/buttons_test.go
··· 41 41 loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 42 42 p := DefaultParams(&loc, time.Now()) 43 43 w := httptest.NewRecorder() 44 - FormatShoppingListHTML(p, multiRecipeList, true, w) 44 + FormatShoppingListHTML(t.Context(), p, multiRecipeList, true, w) 45 45 html := w.Body.String() 46 46 47 47 // Verify HTML is valid ··· 127 127 p := DefaultParams(&loc, time.Now()) 128 128 p.Saved = []ai.Recipe{listWithSavedRecipe.Recipes[0]} 129 129 w := httptest.NewRecorder() 130 - FormatShoppingListHTML(p, listWithSavedRecipe, true, w) 130 + FormatShoppingListHTML(t.Context(), p, listWithSavedRecipe, true, w) 131 131 html := w.Body.String() 132 132 133 133 if !strings.Contains(html, `hx-post="/recipes/`) || !strings.Contains(html, `/finalize"`) {
+8 -7
internal/recipes/html.go
··· 1 1 package recipes 2 2 3 3 import ( 4 + "context" 4 5 "html/template" 5 6 "io" 6 7 "net/http" ··· 41 42 } 42 43 43 44 // FormatShoppingListHTML renders the multi-recipe shopping list view. 44 - func FormatShoppingListHTML(p *generatorParams, l ai.ShoppingList, signedIn bool, writer http.ResponseWriter) { 45 - FormatShoppingListHTMLForHash(p, l, nil, signedIn, p.Hash(), writer) 45 + func FormatShoppingListHTML(ctx context.Context, p *generatorParams, l ai.ShoppingList, signedIn bool, writer http.ResponseWriter) { 46 + FormatShoppingListHTMLForHash(ctx, p, l, nil, signedIn, p.Hash(), writer) 46 47 } 47 48 48 49 // FormatShoppingListHTMLForHash renders the multi-recipe shopping list view for a specific hash. 49 - func FormatShoppingListHTMLForHash(p *generatorParams, l ai.ShoppingList, wineRecommendations map[string]*ai.WineSelection, signedIn bool, hash string, writer http.ResponseWriter) { 50 + func FormatShoppingListHTMLForHash(ctx context.Context, p *generatorParams, l ai.ShoppingList, wineRecommendations map[string]*ai.WineSelection, signedIn bool, hash string, writer http.ResponseWriter) { 50 51 dismissedHashes := make(map[string]bool, len(p.Dismissed)) 51 52 for _, recipe := range p.Dismissed { 52 53 dismissedHashes[recipe.ComputeHash()] = true ··· 93 94 }{ 94 95 Location: *p.Location, 95 96 Date: p.Date.Format("2006-01-02"), 96 - ClarityScript: templates.ClarityScript(), 97 + ClarityScript: templates.ClarityScript(ctx), 97 98 GoogleTagScript: templates.GoogleTagScript(), 98 99 Instructions: p.Instructions, 99 100 Hash: hash, ··· 110 111 } 111 112 } 112 113 113 - // FormatRecipeHTML renders a single recipe view. 114 - func FormatRecipeHTML(p *generatorParams, recipe ai.Recipe, signedIn bool, thread []RecipeThreadEntry, feedback RecipeFeedback, wineRecommendation *ai.WineSelection, writer http.ResponseWriter) { 114 + // FormatRecipeHTML renders a single recipe view with a browser session id for analytics. 115 + func FormatRecipeHTML(ctx context.Context, p *generatorParams, recipe ai.Recipe, signedIn bool, thread []RecipeThreadEntry, feedback RecipeFeedback, wineRecommendation *ai.WineSelection, writer http.ResponseWriter) { 115 116 slices.SortFunc(thread, func(i, j RecipeThreadEntry) int { 116 117 return j.CreatedAt.Compare(i.CreatedAt) 117 118 }) ··· 133 134 }{ 134 135 Location: *p.Location, 135 136 Date: p.Date.Format("2006-01-02"), 136 - ClarityScript: templates.ClarityScript(), 137 + ClarityScript: templates.ClarityScript(ctx), 137 138 GoogleTagScript: templates.GoogleTagScript(), 138 139 Recipe: recipe, 139 140 DisplayIngredients: ingredientsForDisplay(recipe.Ingredients, wineRecommendation),
+31 -11
internal/recipes/html_test.go
··· 12 12 "careme/internal/ai" 13 13 "careme/internal/config" 14 14 "careme/internal/locations" 15 + "careme/internal/logsetup" 15 16 "careme/internal/templates" 16 17 17 18 "golang.org/x/net/html" ··· 59 60 loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 60 61 p := DefaultParams(&loc, time.Now()) 61 62 w := httptest.NewRecorder() 62 - FormatShoppingListHTML(p, list, true, w) 63 + FormatShoppingListHTML(t.Context(), p, list, true, w) 63 64 html := w.Body.String() 64 65 if w.Code != http.StatusOK { 65 66 t.Error("Want ok statuscode") ··· 92 93 loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 93 94 p := DefaultParams(&loc, time.Now()) 94 95 w := httptest.NewRecorder() 95 - FormatShoppingListHTML(p, list, true, w) 96 + FormatShoppingListHTML(t.Context(), p, list, true, w) 96 97 html := w.Body.String() 97 98 98 99 isValidHTML(t, html) ··· 107 108 108 109 templates.Clarityproject = "test456" 109 110 w := httptest.NewRecorder() 110 - FormatShoppingListHTML(p, list, true, w) 111 + FormatShoppingListHTML(t.Context(), p, list, true, w) 111 112 if !bytes.Contains(w.Body.Bytes(), []byte("www.clarity.ms/tag/")) { 112 113 t.Error("HTML should contain Clarity script URL") 113 114 } ··· 117 118 } 118 119 } 119 120 121 + func TestFormatShoppingListHTML_IncludesClaritySessionID(t *testing.T) { 122 + loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 123 + p := DefaultParams(&loc, time.Now()) 124 + 125 + prev := templates.Clarityproject 126 + t.Cleanup(func() { 127 + templates.Clarityproject = prev 128 + }) 129 + templates.Clarityproject = "test456" 130 + 131 + ctx := logsetup.WithSessionID(t.Context(), "sess-123") 132 + 133 + w := httptest.NewRecorder() 134 + FormatShoppingListHTMLForHash(ctx, p, list, nil, true, p.Hash(), w) 135 + if !bytes.Contains(w.Body.Bytes(), []byte(`window.clarity("identify", "sess-123", "sess-123")`)) { 136 + t.Error("HTML should include Clarity identify call with session id") 137 + } 138 + } 139 + 120 140 func TestFormatShoppingListHTML_NoClarityWhenEmpty(t *testing.T) { 121 141 loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 122 142 p := DefaultParams(&loc, time.Now()) 123 143 templates.Clarityproject = "" 124 144 w := httptest.NewRecorder() 125 - FormatShoppingListHTML(p, list, true, w) 145 + FormatShoppingListHTML(t.Context(), p, list, true, w) 126 146 if bytes.Contains(w.Body.Bytes(), []byte("clarity.ms")) { 127 147 t.Error("HTML should not contain Clarity script when project ID is empty") 128 148 } ··· 138 158 }) 139 159 templates.GoogleTagID = "AW-1234567890" 140 160 w := httptest.NewRecorder() 141 - FormatShoppingListHTML(p, list, true, w) 161 + FormatShoppingListHTML(t.Context(), p, list, true, w) 142 162 if !bytes.Contains(w.Body.Bytes(), []byte("www.googletagmanager.com/gtag/js?id=AW-1234567890")) { 143 163 t.Error("HTML should contain Google tag script URL") 144 164 } ··· 157 177 }) 158 178 templates.GoogleTagID = "" 159 179 w := httptest.NewRecorder() 160 - FormatShoppingListHTML(p, list, true, w) 180 + FormatShoppingListHTML(t.Context(), p, list, true, w) 161 181 if bytes.Contains(w.Body.Bytes(), []byte("googletagmanager.com")) { 162 182 t.Error("HTML should not contain Google tag script when tag ID is empty") 163 183 } ··· 167 187 loc := locations.Location{ID: "70000001", Name: "Store", Address: "1 Main St"} 168 188 p := DefaultParams(&loc, time.Now()) 169 189 w := httptest.NewRecorder() 170 - FormatShoppingListHTML(p, list, true, w) 190 + FormatShoppingListHTML(t.Context(), p, list, true, w) 171 191 html := w.Body.String() 172 192 173 193 // Verify "Careme Recipes" is a link to home page ··· 184 204 p := DefaultParams(&loc, time.Now()) 185 205 p.ConversationID = "convo123" 186 206 w := httptest.NewRecorder() 187 - FormatRecipeHTML(p, list.Recipes[0], true, []RecipeThreadEntry{}, RecipeFeedback{}, nil, w) 207 + FormatRecipeHTML(t.Context(), p, list.Recipes[0], true, []RecipeThreadEntry{}, RecipeFeedback{}, nil, w) 188 208 html := w.Body.String() 189 209 190 210 isValidHTML(t, html) ··· 247 267 p := DefaultParams(&loc, time.Now()) 248 268 p.ConversationID = "convo123" 249 269 w := httptest.NewRecorder() 250 - FormatRecipeHTML(p, list.Recipes[0], false, []RecipeThreadEntry{}, RecipeFeedback{}, nil, w) 270 + FormatRecipeHTML(t.Context(), p, list.Recipes[0], false, []RecipeThreadEntry{}, RecipeFeedback{}, nil, w) 251 271 html := w.Body.String() 252 272 253 273 isValidHTML(t, html) ··· 265 285 p := DefaultParams(&loc, time.Now()) 266 286 p.ConversationID = "convo123" 267 287 w := httptest.NewRecorder() 268 - FormatRecipeHTML(p, list.Recipes[0], true, []RecipeThreadEntry{}, RecipeFeedback{}, &ai.WineSelection{ 288 + FormatRecipeHTML(t.Context(), p, list.Recipes[0], true, []RecipeThreadEntry{}, RecipeFeedback{}, &ai.WineSelection{ 269 289 Wines: []ai.Ingredient{ 270 290 {Name: "Oregon Pinot Noir", Price: "$14.99"}, 271 291 {Name: "Backup Chardonnay", Price: "$11.99"}, ··· 322 342 pickerPreviewID := shoppingWinePreviewDOMID(pickerHash) 323 343 pickerDetailID, pickerDetailButtonID := shoppingWineDetailDOMIDs(pickerHash) 324 344 w := httptest.NewRecorder() 325 - FormatShoppingListHTMLForHash(p, multi, map[string]*ai.WineSelection{ 345 + FormatShoppingListHTMLForHash(t.Context(), p, multi, map[string]*ai.WineSelection{ 326 346 wineHash: { 327 347 Wines: []ai.Ingredient{ 328 348 {Name: "Cellar Red", Quantity: "1 bottle", Price: "$15"},
+4 -4
internal/recipes/server.go
··· 144 144 ID: "", 145 145 Name: "Unknown Location", 146 146 }, time.Now()) 147 - FormatRecipeHTML(p, *recipe, signedIn, thread, feedback, wineRecommendation, w) 147 + FormatRecipeHTML(ctx, p, *recipe, signedIn, thread, feedback, wineRecommendation, w) 148 148 return 149 149 } 150 150 // we didn't go back and update old recipes's with new hash so have to handle that here. Could still backfill ··· 173 173 } 174 174 175 175 slog.InfoContext(ctx, "serving shared recipe by hash", "hash", hash, "signedIn", signedIn) 176 - FormatRecipeHTML(p, *recipe, signedIn, thread, feedback, wineRecommendation, w) 176 + FormatRecipeHTML(ctx, p, *recipe, signedIn, thread, feedback, wineRecommendation, w) 177 177 } 178 178 179 179 func (s *server) handleQuestion(w http.ResponseWriter, r *http.Request) { ··· 810 810 }(recipeHash) 811 811 } 812 812 wineWG.Wait() 813 - FormatShoppingListHTMLForHash(p, *slist, wineRecommendations, signedIn, hashParam, w) 813 + FormatShoppingListHTMLForHash(ctx, p, *slist, wineRecommendations, signedIn, hashParam, w) 814 814 return 815 815 } 816 816 ··· 895 895 Style seasons.Style 896 896 RefreshInterval string // seconds 897 897 }{ 898 - ClarityScript: templates.ClarityScript(), 898 + ClarityScript: templates.ClarityScript(ctx), 899 899 GoogleTagScript: templates.GoogleTagScript(), 900 900 Style: seasons.GetCurrentStyle(), 901 901 RefreshInterval: "10", // seconds
+12 -2
internal/templates/templates.go
··· 1 1 package templates 2 2 3 3 import ( 4 + "context" 4 5 "embed" 5 6 "html/template" 6 7 "os" 7 8 8 9 "careme/internal/config" 10 + "careme/internal/logsetup" 9 11 ) 10 12 11 13 //go:embed *.html ··· 62 64 GoogleConversionLabel string 63 65 ) 64 66 65 - // ClarityScript generates the Microsoft Clarity tracking script HTML 66 - func ClarityScript() template.HTML { 67 + // ClarityScript generates the Microsoft Clarity tracking script HTML. 68 + func ClarityScript(ctx context.Context) template.HTML { 67 69 if Clarityproject == "" { 68 70 return "" 69 71 } 72 + sessionID, _ := logsetup.SessionIDFromContext(ctx) 70 73 71 74 script := `<script type="text/javascript"> 72 75 (function(c,l,a,r,i,t,y){ ··· 74 77 t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i; 75 78 y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y); 76 79 })(window, document, "clarity", "script", "` + Clarityproject + `"); 80 + ` 81 + if sessionID != "" { 82 + script += ` 83 + window.clarity("identify", "` + template.JSEscapeString(sessionID) + `", "` + template.JSEscapeString(sessionID) + `"); 84 + ` 85 + } 86 + script += ` 77 87 </script>` 78 88 79 89 return template.HTML(script)
+38
internal/templates/templates_test.go
··· 1 + package templates 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "testing" 7 + 8 + "careme/internal/logsetup" 9 + ) 10 + 11 + func TestClarityScriptIncludesSessionID(t *testing.T) { 12 + prev := Clarityproject 13 + t.Cleanup(func() { 14 + Clarityproject = prev 15 + }) 16 + Clarityproject = "proj-123" 17 + 18 + script := string(ClarityScript(logsetup.WithSessionID(context.Background(), "sess-123"))) 19 + if !strings.Contains(script, `www.clarity.ms/tag/`) { 20 + t.Fatal("expected clarity script url") 21 + } 22 + if !strings.Contains(script, `window.clarity("identify", "sess-123", "sess-123")`) { 23 + t.Fatalf("expected identify call in script, got %q", script) 24 + } 25 + } 26 + 27 + func TestClarityScriptOmitsIdentifyWhenSessionIDEmpty(t *testing.T) { 28 + prev := Clarityproject 29 + t.Cleanup(func() { 30 + Clarityproject = prev 31 + }) 32 + Clarityproject = "proj-123" 33 + 34 + script := string(ClarityScript(context.Background())) 35 + if strings.Contains(script, `window.clarity("identify"`) { 36 + t.Fatalf("did not expect identify call in script, got %q", script) 37 + } 38 + }
+1 -1
internal/users/server.go
··· 179 179 Style seasons.Style 180 180 ServerSignedIn bool 181 181 }{ 182 - ClarityScript: templates.ClarityScript(), 182 + ClarityScript: templates.ClarityScript(ctx), 183 183 GoogleTagScript: templates.GoogleTagScript(), 184 184 User: userForTemplate, 185 185 Success: success,