Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/deeplink/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func handleDashboard(service *deeplink.Service, tmpl *template.Template, logger
for _, linkType := range service.Types() {
var cursor uint64
for {
links, next, err := cfg.Store.List(r.Context(), linkType, cfg.DefaultEnvironment, cursor, 100)
links, next, err := cfg.Store.List(r.Context(), linkType, cursor, 100)
if err != nil {
logger.Error("dashboard: failed to list links", "error", err, "type", linkType)
break
Expand Down
7 changes: 0 additions & 7 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ type Config struct {
// IDLength is the length of generated short IDs. Default: 17.
IDLength int

// DefaultEnvironment is used when no environment is specified
// in the request. Default: "dev".
DefaultEnvironment string

// Store is the persistence backend for links. Required.
Store Store

Expand Down Expand Up @@ -62,9 +58,6 @@ func (c *Config) defaults() {
if c.IDLength == 0 {
c.IDLength = 17
}
if c.DefaultEnvironment == "" {
c.DefaultEnvironment = "dev"
}
if c.BaseURL != "" && !strings.HasSuffix(c.BaseURL, "/") {
c.BaseURL += "/"
}
Expand Down
4 changes: 2 additions & 2 deletions memory_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (s *MemoryStore) Clicks(_ context.Context, id string) (int64, error) {
return s.clicks[id], nil
}

func (s *MemoryStore) List(_ context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error) {
func (s *MemoryStore) List(_ context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error) {
s.mu.RLock()
defer s.mu.RUnlock()

Expand All @@ -70,7 +70,7 @@ func (s *MemoryStore) List(_ context.Context, linkType, environment string, curs

ids := make([]string, 0, len(s.payload))
for id, payload := range s.payload {
if payload.Type == linkType && payload.Environment == environment {
if payload.Type == linkType {
ids = append(ids, id)
}
}
Expand Down
2 changes: 0 additions & 2 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ type Link struct {
// ImageURL for the OG preview image.
ImageURL string `json:"image_url,omitempty"`

// Environment groups links (e.g. "dev", "staging", "production").
Environment string `json:"environment,omitempty"`
// CreatedAt is an RFC 3339 timestamp. Set by the service.
CreatedAt string `json:"created_at,omitempty"`

Expand Down
4 changes: 2 additions & 2 deletions redis_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func (s *RedisStore) Clicks(ctx context.Context, id string) (int64, error) {
return n, err
}

func (s *RedisStore) List(ctx context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error) {
func (s *RedisStore) List(ctx context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error) {
scanCount := max(count, 100)

keys, nextCursor, err := s.client.Scan(ctx, cursor, s.scanPattern(), scanCount).Result()
Expand Down Expand Up @@ -118,7 +118,7 @@ func (s *RedisStore) List(ctx context.Context, linkType, environment string, cur
continue
}

if p.Type == linkType && p.Environment == environment {
if p.Type == linkType {
clicks, _ := clickCmds[key].Int64()
links = append(links, LinkInfo{
ShortLink: s.stripPrefix(key),
Expand Down
41 changes: 12 additions & 29 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
//
// POST /shorten create a short link
// GET /{shortID} preview page (or 302 if no template)
// GET /links/{type} list links by type and environment
// GET /links/{type} list links by type
// GET /links/{type}/{shortID} single link with click count
// GET /health health check
//
Expand Down Expand Up @@ -45,7 +45,6 @@ func (s *Service) Handler() http.Handler {

func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) {
start := time.Now()
env := s.envFromRequest(r)

r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB
var payload Link
Expand All @@ -54,8 +53,6 @@ func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) {
return
}

payload.Environment = env

processor := s.registry.Get(strings.ToLower(payload.Type))
if processor == nil {
s.respondError(w, ErrInvalidType)
Expand All @@ -69,18 +66,17 @@ func (s *Service) handleGenerate(w http.ResponseWriter, r *http.Request) {

shortURL, err := s.shortenURL(r.Context(), &payload)
if err != nil {
s.config.Logger.Error("failed to shorten URL", "error", err, "type", payload.Type, "env", env)
s.config.Logger.Error("failed to shorten URL", "error", err, "type", payload.Type)
s.respondError(w, NewError(err, http.StatusInternalServerError, "failed to shorten URL"))
return
}

respondJSON(w, http.StatusCreated, map[string]string{"short_url": shortURL})
s.config.Logger.Info("link generated", "shortID", strings.TrimPrefix(shortURL, s.config.BaseURL), "type", payload.Type, "env", env, "duration", time.Since(start))
s.config.Logger.Info("link generated", "shortID", strings.TrimPrefix(shortURL, s.config.BaseURL), "type", payload.Type, "duration", time.Since(start))
}

func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) {
start := time.Now()
env := s.envFromRequest(r)

shortID := r.PathValue("shortID")
if s.skipPath(shortID) {
Expand All @@ -90,7 +86,7 @@ func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) {

payload, err := s.expandURL(r.Context(), shortID)
if err != nil {
s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID, "env", env)
s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID)
http.NotFound(w, r)
return
}
Expand All @@ -113,22 +109,21 @@ func (s *Service) handlePreview(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "public, max-age=3600")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
s.config.Logger.Info("preview rendered", "shortID", shortID, "type", payload.Type, "env", env, "duration", time.Since(start))
s.config.Logger.Info("preview rendered", "shortID", shortID, "type", payload.Type, "duration", time.Since(start))
}

// handleStaticPreview renders a preview page without auto-redirect.
// Useful as an intermediate page for iOS Universal Links or any
// flow where the user should tap to continue.
func (s *Service) handleStaticPreview(w http.ResponseWriter, r *http.Request) {
start := time.Now()
env := s.envFromRequest(r)
w.Header().Set("Content-Type", "text/html")

shortID := r.PathValue("shortID")

payload, err := s.expandURL(r.Context(), shortID)
if err != nil {
s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID, "env", env)
s.config.Logger.Error("failed to expand URL", "error", err, "shortID", shortID)
http.NotFound(w, r)
return
}
Expand All @@ -149,7 +144,7 @@ func (s *Service) handleStaticPreview(w http.ResponseWriter, r *http.Request) {

w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf.Bytes())
s.config.Logger.Info("preview rendered (no redirect)", "shortID", shortID, "type", payload.Type, "env", env, "duration", time.Since(start))
s.config.Logger.Info("preview rendered (no redirect)", "shortID", shortID, "type", payload.Type, "duration", time.Since(start))
}

func (s *Service) handleRedirect(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -177,15 +172,14 @@ func (s *Service) handleRedirect(w http.ResponseWriter, r *http.Request) {

func (s *Service) handleLinkList(w http.ResponseWriter, r *http.Request) {
start := time.Now()
env := s.envFromRequest(r)
linkType := r.PathValue("type")

var allLinks []LinkInfo
var cursor uint64
for {
links, next, err := s.config.Store.List(r.Context(), linkType, env, cursor, 100)
links, next, err := s.config.Store.List(r.Context(), linkType, cursor, 100)
if err != nil {
s.config.Logger.Error("failed to list links", "error", err, "type", linkType, "env", env)
s.config.Logger.Error("failed to list links", "error", err, "type", linkType)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
Expand All @@ -209,18 +203,17 @@ func (s *Service) handleLinkList(w http.ResponseWriter, r *http.Request) {
s.config.Logger.Error("failed to encode response", "error", err)
}

s.config.Logger.Info("links listed", "type", linkType, "count", len(allLinks), "env", env, "duration", time.Since(start))
s.config.Logger.Info("links listed", "type", linkType, "count", len(allLinks), "duration", time.Since(start))
}

func (s *Service) handleLinkDetail(w http.ResponseWriter, r *http.Request) {
start := time.Now()
env := s.envFromRequest(r)
linkType := r.PathValue("type")
shortID := r.PathValue("shortID")

payload, err := s.config.Store.Get(r.Context(), shortID)
if err != nil {
s.config.Logger.Warn("link not found", "shortId", shortID, "env", env)
s.config.Logger.Warn("link not found", "shortId", shortID)
http.Error(w, "link not found", http.StatusNotFound)
return
}
Expand All @@ -237,7 +230,7 @@ func (s *Service) handleLinkDetail(w http.ResponseWriter, r *http.Request) {
s.config.Logger.Error("failed to encode response", "error", err)
}

s.config.Logger.Info("link detail fetched", "shortId", shortID, "env", env, "duration", time.Since(start))
s.config.Logger.Info("link detail fetched", "shortId", shortID, "duration", time.Since(start))
}

func (s *Service) handleWellKnown(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -274,16 +267,6 @@ func (s *Service) buildPreviewData(payload *Link) any {
return payload
}

func (s *Service) envFromRequest(r *http.Request) string {
if env := r.Header.Get("X-Environment"); env != "" {
return env
}
if env := r.URL.Query().Get("environment"); env != "" {
return env
}
return s.config.DefaultEnvironment
}

func (s *Service) withCORS(h http.HandlerFunc) http.HandlerFunc {
allowed := make(map[string]bool, len(s.config.AllowedOrigins))
for _, o := range s.config.AllowedOrigins {
Expand Down
4 changes: 2 additions & 2 deletions store.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ type Store interface {
IncrClick(ctx context.Context, id string) (int64, error)
// Clicks returns the current click count for id.
Clicks(ctx context.Context, id string) (int64, error)
// List returns links matching linkType and environment.
// List returns links matching linkType.
// Pass cursor 0 to start. The returned cursor is 0 when there are
// no more results. Cursor values are opaque and store-specific;
// do not assume sequential offsets.
// Returned [LinkInfo.ShortLink] values contain only the short ID (no base URL).
List(ctx context.Context, linkType, environment string, cursor uint64, count int64) ([]LinkInfo, uint64, error)
List(ctx context.Context, linkType string, cursor uint64, count int64) ([]LinkInfo, uint64, error)
}
Loading