Create clicks table and tracking (#36) #42
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "36-clicks-table-tracking"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Create a
clickstable to record link usage and implement a redirect-through tracking pattern. Clicking a link card now POSTs to a tracking endpoint, records the click, then redirects to the actual URL.Changes
db/migrate/20260609195232_create_clicks.rb-- migration creating clicks table with link_id (FK), user_id (nullable, no FK constraint), session_id, clicked_at, plus indicesapp/models/click.rb-- belongs_to :link, validates at least one of user_id or session_id is presentapp/models/link.rb-- added has_many :clicks (dependent: :destroy) and#click_countmethodapp/controllers/clicks_controller.rb-- POST create: finds link, records click with session_id, redirects to link.urlconfig/routes.rb-- addedpost :click, on: :member, to: "clicks#create"nested under linksapp/views/links/_link.html.erb-- changed link title from directlink_totobutton_tothat POSTs through click trackingapp/views/links/show.html.erb-- same tracking pattern, plus displays click countdb/schema.rb-- updated with clicks table schematest/models/click_test.rb-- 7 tests covering all model validationstest/controllers/clicks_controller_test.rb-- 3 tests covering recording, redirect, and 404 behaviortest/fixtures/clicks.yml-- fixture data for testsTest Plan
bin/rails test-- 14 tests, 28 assertions, 0 failures, 0 errorsbin/rails db:migrate-- migration runs cleanly on both test and developmentReview Checklist
clicks.countvia methodRelated Notes
None.
Related
Closes #36
PR Review -- #42 Create clicks table and tracking
Migration
create_clicks.rb-- Schema matches spec exactly:link_idwith FK,user_idas nullable bigint with no FK constraint,session_idstring,clicked_atNOT NULL. Indices on all lookup columns. Clean.Model layer
Click--belongs_to :link, validatesclicked_atpresence, custom validation ensures at least one ofuser_id/session_id. Correct.Link--has_many :clicks, dependent: :destroyand#click_countusingclicks.count(no counter_cache per spec). Correct.Controller
allow_other_host: truefor external URLs. Session ID fallback toSecureRandom.hex(16)whensession.idis nil (handles edge cases in tests and fresh sessions). No unnecessary logic.Routes
post :click, on: :member, to: "clicks#create"nested under links -- generatesPOST /links/:id/click. Correct.Views
_link.html.erbandshow.html.erbchanged fromlink_totobutton_towith POST method for redirect-through tracking. Show page displays click count. Correct.Tests
Note for future
click_countissues aSELECT COUNT(*)per link. On the index page this is N+1. Acceptable now (spec says no counter_cache), but worth a.includes/counter_cache optimization when click volume grows.VERDICT: APPROVE -- Implementation matches the spec in #36 across all file targets, constraints are respected, tests are comprehensive.
PR #42 Review
DOMAIN REVIEW
Tech stack: Ruby on Rails 8.1, PostgreSQL, Hotwire/Turbo, Minitest. This is a standard Rails MVC feature -- new model, controller, migration, views, and tests.
Migration (
db/migrate/20260609195232_create_clicks.rb)link_idhas a FK constraint;user_idintentionally omits one (users table does not exist yet). Indices onuser_id,session_id,clicked_atare appropriate for the anticipated queries.clicked_atisnull: falseat the DB level, matching the model validation. Good.user_idisbigintbut there is no users table yet. When the users table lands, a follow-up migration should add the FK constraint. This is documented in the PR body -- acceptable.Model (
app/models/click.rb)must_have_attributioncorrectly enforces at least one identifier. Uses.blank?which covers both nil and empty string -- correct.belongs_to :linkis non-optional by default in Rails 8.1, so the FK presence is validated automatically. Good.Model (
app/models/link.rb)has_many :clicks, dependent: :destroy-- correct. When a link is deleted, its clicks are cleaned up.click_countmethod usesclicks.count, which issuesSELECT COUNT(*)each time. This is fine at current scale. When click volume grows, acounter_cachecolumn would be more efficient, but the PR body explicitly acknowledges this trade-off. Acceptable.Controller (
app/controllers/clicks_controller.rb)Link.find(params[:id])raisesRecordNotFoundon miss, which Rails maps to 404. Matches existing patterns inLinksController.Click.create!with bang -- if the create fails validation, this raisesActiveRecord::RecordInvalidwhich would 500. Sincesession_idis always populated bycurrent_session_idandclicked_atis always set, the only failure path would be if the link somehow became nil betweenfindandcreate!, which is a race condition not worth guarding against. Acceptable.redirect_to link.url, allow_other_host: true-- Theallow_other_host: trueis required since links are external URLs. This is NOT an open redirect vulnerability because the URL comes from a storedLinkrecord whoseurlfield is validated againsthttp/httpsschemes at creation time. Users cannot control the redirect target through request parameters.current_session_id-- falls back toSecureRandom.hex(16)whensession.idis nil. This means a user without a session cookie gets a random ID that is not persisted across requests. Each click from a cookieless client gets a different session_id. This is a known limitation, not a bug.Views
button_togenerates a form with a POST, which is semantically correct for a state-changing action (recording a click). CSRF protection is handled automatically by Rails. Good.link_to->button_tochange means the title is now a submit button inside a form. CSS classeslink-card-trackandlink-card-track-formare added for styling. The styling for these classes is not included in this PR -- this may cause visual regression if the button renders with default browser button styling.Routes
post :click, on: :member, to: "clicks#create"nested under links producesPOST /links/:id/clickmapped toClicksController#create. Clean and RESTful.Tests
BLOCKERS
None found.
params[:id]goes throughLink.find(parameterized query).session_idis generated server-side.NITS
Missing button CSS -- The
link-card-trackandlink-detail-trackCSS classes are referenced but no stylesheet changes are included. Thebutton_tohelper renders an actual<button>element, which has default browser styling (border, background, padding). Without CSS to reset these, the link titles will look like clickable buttons rather than text links. Consider adding CSS in this PR or documenting it as a known follow-up.N+1 potential on index --
app/views/links/index.html.erbrenders each link via the_link.html.erbpartial. The partial now usesclick_link_path(link)which only needs the link ID (no extra query). However, if click_count is ever added to the index cards, anincludes(:clicks)or counter_cache will be needed. No action required now, just a note.current_session_idfallback -- Whensession.idis nil, the fallback generates a random hex that is not stored anywhere. This means the generated session_id cannot be correlated across requests for cookieless clients. If analytics accuracy matters, consider initializing the session to force a stable ID. Low priority.Fixture naming --
clicks.ymluses generic namesone,two,three. The links fixtures use descriptive names (alpha,bravo). Consider naming click fixtures descriptively (e.g.,alpha_session_click,alpha_user_click,bravo_click) for readability. Minor.SOP COMPLIANCE
db/schema.rbis updated (required when committing migrations)PROCESS OBSERVATIONS
user_idcolumn is pre-placed without FK for future users table. The PR body documents this explicitly. When issue #32 (Create users table) ships, a follow-up migration adding the FK constraint should be tracked.bin/rails db:migrateon deploy. No data migration needed.VERDICT: APPROVED
PR #42 Re-Review
Re-review after nit fixes from commit
7be4d0a. Verifying 3 reported fixes from the previous review.NIT FIX VERIFICATION
Nit 1 -- CSS resets for button_to elements: VERIFIED
The diff adds
.link-card-track-form/.link-detail-track-formwithdisplay: inlineand.link-card-track/.link-detail-trackwith a full button reset (background, border, padding, margin, font, color, cursor, text-align, text-decoration all zeroed out). Hover states usevar(--color-accent)consistent with the existing.link-card-title a:hoverpattern. Theform: { class: "..." }parameter onbutton_tocorrectly targets the wrapping<form>element. Well done.Nit 2 -- N+1 query potential: ACKNOWLEDGED SKIP
Original review noted this was a future concern, not applicable to current code. The
click_countmethod on show.html.erb only fires for a single link, and the index partial does not display click counts. No action needed now. Correct to skip.Nit 3 -- current_session_id persistence: VERIFIED
The controller now reads/writes
session[:click_tracking_id]with the||=pattern:session[:click_tracking_id] ||= session.id&.to_s || SecureRandom.hex(16). This correctly persists the generated ID so subsequent requests within the same session get the same tracking ID. The&.to_ssafely handles nil session IDs (e.g., in test environments or before session initialization).Nit 4 -- Fixture renaming: VERIFIED
Fixtures renamed from generic
one/two/threeto descriptivealpha_recent,alpha_old,bravo_recent. Names now communicate which link they belong to and their temporal relationship. Fixture references in tests (links(:alpha)etc.) align with the links fixture file.DOMAIN REVIEW
Stack: Ruby on Rails 8.1, Minitest, PostgreSQL
Migration (db/migrate/20260609195232_create_clicks.rb)
t.references :link, null: false, foreign_key: truefor the link FKuser_idisbigintwith no FK constraint -- intentional and documented (users table does not exist yet)user_id,session_id, andclicked_atare appropriate for the query patterns (lookup by user, lookup by session, time-range queries)clicked_atisdatetime, null: false-- good, prevents null timestampsModel (app/models/click.rb)
belongs_to :linkwith custom validationmust_have_attributionensuring at least one of user_id/session_id -- correct pattern for a pre-auth system where users table is pendingvalidates :clicked_at, presence: true-- correctModel (app/models/link.rb)
has_many :clicks, dependent: :destroy-- correct cascade behavior for when links are deletedclick_countdelegates toclicks.count-- issues aSELECT COUNT(*)which is fine for single-record displayController (app/controllers/clicks_controller.rb)
Link.find(params[:id])raisesActiveRecord::RecordNotFoundwhich Rails maps to 404 -- confirmed by the testClick.create!with bang -- raises on validation failure. Since session_id is always populated bycurrent_session_id, andclicked_atis alwaysTime.current, validation failure should not occur in normal flow. Acceptable.redirect_to link.url, allow_other_host: true-- necessary for external URLs. The URL is already validated at the model level viaURI::DEFAULT_PARSER.make_regexp(%w[http https])on Link, so this is safe.button_togenerates a form with Rails' authenticity token by default.Views
button_towithmethod: :postcorrectly generates a form that POSTs rather than navigating directly -- this is the standard Rails pattern for tracking through redirect.pluralize(@link.click_count)handles singular/plural correctly.Routes
post :click, on: :member, to: "clicks#create"nested under links -- generates/links/:id/clickwhich is clean and RESTful.Tests (10 tests total: 7 model + 3 controller)
assert_difference "Click.count", 1pattern correctly verifies persistence.BLOCKERS
None.
NITS
None remaining. All 3 actionable nits from the previous review have been addressed correctly.
SOP COMPLIANCE
db/schema.rbupdated to match migrationPROCESS OBSERVATIONS
Clean implementation of a tracking table with forward-compatible design (user_id column ready for auth integration). The decision to skip FK on user_id is documented in the PR body and makes sense given the users table does not exist yet. The button_to-with-redirect pattern is the standard Rails approach for click-through tracking without JavaScript dependencies.
VERDICT: APPROVED
PR #42 Review
DOMAIN REVIEW
Stack: Ruby on Rails 8.1, Minitest, PostgreSQL, Turbo/Importmaps. Reviewing as a Rails domain expert.
Architecture: The redirect-through-tracking pattern (POST -> record -> redirect) is sound. Using
button_toto generate a form that POSTs is the correct Rails approach -- it gets CSRF protection for free via the authenticity token thatbutton_toembeds automatically.Migration: Clean and correct. FK on
link_id, no FK onuser_id(documented as intentional since users table does not exist yet), appropriate indices on all lookup columns. Theclicked_atcolumn withnull: falseis good -- the model validates this too.Model: The
must_have_attributioncustom validator is well-structured.belongs_to :linkenforces presence by default in Rails 5+.dependent: :destroyon thehas_many :clicksis correct for cleanup.Session tracking:
current_session_idhas good fallback logic:session[:click_tracking_id] ||= session.id&.to_s || SecureRandom.hex(16). This ensures a value is always present, which meansClick.create!will never fail the attribution validator for anonymous users.BLOCKERS
None. No blockers identified.
NITS
N+1 risk in
click_count(low priority now, high priority later):Link#click_countexecutesclicks.countwhich fires aSELECT COUNT(*)per call. Currently only used onshow.html.erb(single link), so it is a single query. But if click count is ever added to the index partial (_link.html.erb), it will create an N+1. Consider adding a comment to the method noting this, or plan acounter_cachecolumn when click counts appear on index pages.app/models/link.rb, theclick_countmethodNo rate limiting on click tracking endpoint: Any anonymous user can POST to
/links/:id/clickin a tight loop to inflate counts. This is not a security vulnerability but an analytics-integrity concern. Consider whether throttling (e.g.,rack-attackor a uniqueness constraint on session_id+link_id per time window) is needed before click data drives any real decisions.CSS DRY opportunity: The
.link-card-track:hoverand.link-detail-track:hoverrules are identical. These could be combined:app/assets/stylesheets/application.cssclicked_atvscreated_atredundancy: Theclickstable has bothclicked_at(explicit) andcreated_at(Rails timestamps). In practice these will always be the same value since clicks are recorded at the moment they happen. The explicitclicked_atdoes communicate intent more clearly thancreated_at, so this is defensible, but worth noting the redundancy.SOP COMPLIANCE
36-clicks-table-trackingreferences issue #36Closes #36present in Related section.envfiles, or credentials committedPROCESS OBSERVATIONS
link_toforbutton_to. The existing link CRUD is untouched.user_idcolumn without FK constraint is explicitly documented and matches the current state of the app (no users table yet). When#32(Create users table) lands, a follow-up migration should add the FK constraint. Consider creating a ticket for this or adding a TODO comment in the migration.VERDICT: APPROVED