affiliate_tracker 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 825b530c03d5358a02b7c718dea8a983a28b6f7d410708baaadedcd8dd085983
4
+ data.tar.gz: 540f309d350fd3326c5668fda1101a8212871b1283e3b2858dfd41d790c11658
5
+ SHA512:
6
+ metadata.gz: 37821ddde6e55a7522546626e1e2a5527dde7e929048573401c46d4b3e94e97ae2e92ea911e663a12da7ed200a0c9bd638642d098cdaf226ee00059b664690dc
7
+ data.tar.gz: 335f997f6b7da97d732e2d27bd7915ed78bd35b4546b66418f60789b3cea2c44d2c0bf0d11c40dbdb56066cfb996889062f5398317d45c2c3f3fdc101d51d796
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.2
3
+ NewCops: enable
4
+
5
+ Style/StringLiterals:
6
+ EnforcedStyle: double_quotes
7
+
8
+ Style/StringLiteralsInInterpolation:
9
+ EnforcedStyle: double_quotes
10
+
11
+ Layout/LineLength:
12
+ Max: 140
13
+
14
+ Metrics/BlockLength:
15
+ Exclude:
16
+ - "test/**/*"
17
+
18
+ Metrics/ClassLength:
19
+ Max: 150
20
+
21
+ Metrics/MethodLength:
22
+ Max: 30
23
+
24
+ Metrics/ModuleLength:
25
+ Max: 150
26
+
27
+ Metrics/AbcSize:
28
+ Max: 35
29
+
30
+ Metrics/CyclomaticComplexity:
31
+ Max: 15
32
+
33
+ Metrics/PerceivedComplexity:
34
+ Max: 15
35
+
36
+ Style/Documentation:
37
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.4.4
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.2
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
1
+ # Changelog
2
+
3
+ ## [0.3.1] - 2026-03-17
4
+
5
+ ### Changed
6
+ - Redirect status changed from 301 (`:moved_permanently`) to 302 (`:found`). 301s are cached permanently by browsers, which prevents click re-tracking on subsequent visits.
7
+ - IPv6 anonymization: `anonymize_ip` now handles IPv6 addresses by zeroing the last 80 bits (last 5 groups), in addition to the existing IPv4 last-octet zeroing.
8
+ - CI matrix expanded to test Ruby 3.2, 3.3, and 3.4.
9
+ - Gem prepared for RubyGems.org publication: added LICENSE.txt, CHANGELOG.md to gem files, MFA requirement, upper-bound Rails dependency (`< 10`).
10
+
11
+ ## [0.3.0] - 2026-03-17
12
+
13
+ ### Added
14
+ - URL normalization: URLs without a protocol (e.g. `shop.com/page`) are automatically prepended with `https://` both at URL generation time and at redirect time. Prevents broken redirects for malformed destination URLs.
15
+ - Tests for URL normalization in `UrlGenerator` and `ClicksController`.
16
+
17
+ ### Changed
18
+ - `UrlGenerator#initialize` now normalizes destination URLs before encoding.
19
+ - `ClicksController#redirect` normalizes destination URLs before appending UTM params and redirecting.
20
+
21
+ ## [0.2.0] - 2026-01-24
22
+
23
+ - Per-link metadata override for UTM params (`utm_source`, `utm_medium`).
24
+ - Configurable `after_click` callback.
25
+ - Proc-based `fallback_url` with untrusted payload data.
26
+ - Click deduplication (same IP + URL within 5s).
27
+
28
+ ## [0.1.0] - 2025-12-15
29
+
30
+ - Initial release with click tracking, UTM injection, and redirect.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Justyna Wojtczak
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,240 @@
1
+ # AffiliateTracker
2
+
3
+ Click tracking for affiliates working with small e-commerce shops. Track your clicks, add UTM params, prove your value.
4
+
5
+ ## Who is this for?
6
+
7
+ **You're an affiliate/influencer** who promotes products from small shops (Shoplo, Shoper, WooCommerce, IdoSell, etc.) that **don't have their own affiliate system**.
8
+
9
+ You send traffic via newsletters, blogs, or social media — but you have no way to prove how many clicks you actually sent. The shop owner sees some visits in Google Analytics, but can't tell which came from you.
10
+
11
+ **This gem solves that:**
12
+ - You track every click on your side (proof for negotiations)
13
+ - Links automatically include UTM parameters (shop sees your traffic in their GA)
14
+ - Optional `ref=` parameter for shops that support simple referral tracking
15
+
16
+ **Not for:** Amazon, eBay, or platforms with existing affiliate programs (they have their own tracking and often prohibit link masking).
17
+
18
+ ## Problem
19
+
20
+ ```
21
+ You: "I sent you 500 clicks this month"
22
+ Shop: "Google Analytics shows only 200 visits"
23
+ You: "..."
24
+ ```
25
+
26
+ ## Solution
27
+
28
+ ```
29
+ Your email → User clicks → AffiliateTracker counts → Redirect with UTM → Shop sees source
30
+
31
+ You have proof: 500 clicks
32
+ Shop sees: utm_source=yourname
33
+ ```
34
+
35
+ ## Features
36
+
37
+ - Click tracking with metadata (shop, campaign, etc.)
38
+ - Automatic UTM parameter injection
39
+ - Click deduplication (same IP + URL within 5s counted once)
40
+ - Built-in dashboard
41
+ - Rails 8+ / zero configuration
42
+
43
+ ## Installation
44
+
45
+ ```ruby
46
+ gem "affiliate_tracker"
47
+ ```
48
+
49
+ Or, for the latest development version:
50
+
51
+ ```ruby
52
+ gem "affiliate_tracker", git: "https://github.com/justi-blue/affiliate_tracker"
53
+ ```
54
+
55
+ ```bash
56
+ rails generate affiliate_tracker:install
57
+ rails db:migrate
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### affiliate_link helper
63
+
64
+ ```erb
65
+ <%# Simple link %>
66
+ <%= affiliate_link "https://modago.pl/sukienka", "Zobacz sukienkę" %>
67
+
68
+ <%# With metadata %>
69
+ <%= affiliate_link "https://modago.pl/sukienka", "Zobacz sukienkę",
70
+ shop: "modago",
71
+ campaign: "homepage" %>
72
+
73
+ <%# With CSS classes %>
74
+ <%= affiliate_link "https://modago.pl/sukienka", "Zobacz",
75
+ shop: "modago",
76
+ class: "btn btn-primary" %>
77
+
78
+ <%# Block syntax %>
79
+ <%= affiliate_link "https://modago.pl/sukienka", shop: "modago" do %>
80
+ <img src="photo.jpg"> Zobacz ofertę
81
+ <% end %>
82
+ ```
83
+
84
+ **Generates:**
85
+ ```html
86
+ <a href="https://yourapp.com/a/eyJ...?s=abc" target="_blank" rel="noopener">
87
+ Zobacz sukienkę
88
+ </a>
89
+ ```
90
+
91
+ ### affiliate_url helper (URL only)
92
+
93
+ ```erb
94
+ <a href="<%= affiliate_url 'https://modago.pl/sukienka', shop: 'modago' %>">
95
+ Custom link
96
+ </a>
97
+ ```
98
+
99
+ ### User Tracking
100
+
101
+ Track which user clicked a link by passing `user_id`:
102
+
103
+ ```erb
104
+ <%# On web pages - use Current.user %>
105
+ <%= affiliate_link "https://shop.com/product", "Buy Now",
106
+ user_id: Current.user&.id,
107
+ campaign: "homepage" %>
108
+
109
+ <%# In mailers - pass user explicitly (Current.user not available in background jobs) %>
110
+ <%= affiliate_link "https://shop.com/product", "View Deal",
111
+ user_id: @user.id,
112
+ shop_id: @shop.id,
113
+ campaign: "daily_digest" %>
114
+ ```
115
+
116
+ Common tracking parameters:
117
+ - `user_id` - User who clicked (for attribution)
118
+ - `shop_id` - Shop identifier
119
+ - `promotion_id` - Specific promotion
120
+ - `campaign` - Campaign name (e.g., "daily_digest", "homepage")
121
+
122
+ ### In Mailers
123
+
124
+ ```erb
125
+ <%# app/views/digest_mailer/weekly.html.erb %>
126
+ <% @promotions.each do |promo| %>
127
+ <%= affiliate_link promo.shop.website_url, "Zobacz promocję",
128
+ user_id: @user.id,
129
+ shop_id: promo.shop.id,
130
+ promotion_id: promo.id,
131
+ campaign: "weekly_digest" %>
132
+ <% end %>
133
+ ```
134
+
135
+ ### Real Example: Shoplo Store
136
+
137
+ ```erb
138
+ <%# Just the product URL - ref param added automatically from config %>
139
+ <%= affiliate_link "https://demo.shoplo.com/koszulka-bawelniana",
140
+ "Zobacz koszulkę",
141
+ shop: "shoplo-demo",
142
+ campaign: "styczen2025" %>
143
+ ```
144
+
145
+ **User clicks → AffiliateTracker counts → Redirects to:**
146
+ ```
147
+ https://demo.shoplo.com/koszulka-bawelniana?ref=partnerJan&utm_source=smartoffers&utm_medium=email&utm_campaign=styczen2025&utm_content=shoplo-demo
148
+ ```
149
+
150
+ The shop sees:
151
+ - `ref=partnerJan` - from `config.ref_param` (automatic)
152
+ - UTM params - in Google Analytics
153
+
154
+ ### Result
155
+
156
+ 1. Generates: `https://yourapp.com/a/eyJ...?s=abc`
157
+ 2. On click, redirects to: `https://modago.pl/sukienka?utm_source=smartoffers&utm_medium=email&utm_campaign=weekly_digest&utm_content=modago`
158
+
159
+ ### Configuration
160
+
161
+ ```ruby
162
+ # config/initializers/affiliate_tracker.rb
163
+ AffiliateTracker.configure do |config|
164
+ # Your brand name (appears in utm_source)
165
+ config.utm_source = "smartoffers"
166
+
167
+ # Default medium
168
+ config.utm_medium = "email"
169
+
170
+ # Referral param (adds ?ref=partnerJan to all links)
171
+ config.ref_param = "partnerJan"
172
+
173
+ # Dashboard auth
174
+ config.authenticate_dashboard = -> {
175
+ redirect_to main_app.login_path unless current_user&.admin?
176
+ }
177
+ end
178
+ ```
179
+
180
+ ### Tracking Parameters
181
+
182
+ | Parameter | Source | Example |
183
+ |-----------|--------|---------|
184
+ | `ref` | `config.ref_param` | `partnerJan` |
185
+ | `utm_source` | `config.utm_source` | `smartoffers` |
186
+ | `utm_medium` | `config.utm_medium` | `email` |
187
+ | `utm_campaign` | `campaign:` in helper | `weekly_digest` |
188
+ | `utm_content` | `shop:` in helper | `modago` |
189
+
190
+ Override defaults per-link:
191
+
192
+ ```erb
193
+ <%= affiliate_url "https://shop.com",
194
+ utm_source: "newsletter",
195
+ utm_medium: "email",
196
+ campaign: "black_friday" %>
197
+ ```
198
+
199
+ ## Dashboard
200
+
201
+ Access at `/a/dashboard`
202
+
203
+ Shows:
204
+ - Total clicks
205
+ - Clicks today/this week
206
+ - Top destinations (shops)
207
+ - Recent clicks with metadata
208
+
209
+ ## Security
210
+
211
+ - Links are signed with **HMAC-SHA256** (128-bit truncated signature per RFC 2104)
212
+ - Signature verification uses constant-time comparison (`ActiveSupport::SecurityUtils`)
213
+ - Invalid or missing signatures result in a **302 redirect** to a configurable fallback URL (default: `/`)
214
+ - Payload is Base64-encoded JSON (not encrypted) — metadata is readable but tamper-proof
215
+
216
+ ### Fallback URL
217
+
218
+ When a user visits a link with an invalid or missing signature (e.g., bots stripping query params), the gem redirects instead of returning an error:
219
+
220
+ ```ruby
221
+ AffiliateTracker.configure do |config|
222
+ # Static URL (default)
223
+ config.fallback_url = "/"
224
+
225
+ # Dynamic — receives decoded payload Hash (unverified, treat as untrusted)
226
+ config.fallback_url = ->(payload) {
227
+ slug = payload&.dig("shop")
228
+ slug.present? ? "/shops/#{slug}" : "/"
229
+ }
230
+ end
231
+ ```
232
+
233
+ ## For Shop Owners
234
+
235
+ Tell your partner shops:
236
+ > "All my links include UTM parameters. Check Google Analytics → Acquisition → Traffic Sources → filter by `utm_source=yourname`"
237
+
238
+ ## License
239
+
240
+ MIT
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -0,0 +1,238 @@
1
+ # Test Suite Tasks
2
+
3
+ ## Goal
4
+
5
+ Bring the gem's test suite closer to real user flows, cover missing edge cases, and reduce low-value duplication.
6
+
7
+ ## Priority 1: Cover the real runtime flow
8
+
9
+ ### Task 1: Add integration tests for click redirect happy path
10
+
11
+ - Add request or integration tests for `AffiliateTracker::ClicksController#redirect`.
12
+ - Verify valid signed URLs:
13
+ - decode correctly,
14
+ - create a click record,
15
+ - redirect to the destination URL,
16
+ - append expected UTM params,
17
+ - preserve existing query params.
18
+ - Verify response status and redirect target exactly.
19
+
20
+ **Why**
21
+
22
+ Current tests mostly cover URL generation, but not the main production flow after a user clicks a link.
23
+
24
+ **Suggested assertions**
25
+
26
+ - click count increases by 1
27
+ - saved `destination_url` matches decoded payload
28
+ - redirect URL contains `utm_source`, `utm_medium`, optional `utm_campaign`, optional `utm_content`
29
+ - existing query params are preserved
30
+
31
+ ### Task 2: Add integration tests for invalid or missing signature
32
+
33
+ - Test invalid signature redirects to fallback URL.
34
+ - Test missing signature redirects to fallback URL.
35
+ - Test tampered payload redirects to fallback URL.
36
+ - Test fallback works for:
37
+ - static string config,
38
+ - proc-based config with decoded untrusted payload,
39
+ - corrupt payload passed into proc fallback.
40
+
41
+ **Why**
42
+
43
+ This is a key resilience path documented in the gem, but it is not covered end-to-end.
44
+
45
+ ### Task 3: Add integration tests for click deduplication
46
+
47
+ - Exercise two requests through the controller, not directly through `Rails.cache`.
48
+ - Verify same IP + same destination within 5 seconds creates only one click.
49
+ - Verify different IP still records a second click.
50
+ - Verify different destination still records a second click.
51
+ - Verify click is recorded again after dedup window expires or is cleared.
52
+
53
+ **Why**
54
+
55
+ Current deduplication tests check cache primitives, not actual dedup behavior in production code.
56
+
57
+ ### Task 4: Add integration tests for `after_click`
58
+
59
+ - Verify configured `after_click` handler is called after a successful click record.
60
+ - Verify handler receives the created `AffiliateTracker::Click`.
61
+ - Verify exceptions in handler do not break redirect flow.
62
+
63
+ **Why**
64
+
65
+ `after_click` is part of the public configuration API and should be regression-safe.
66
+
67
+ ### Task 5: Add integration tests for request metadata handling
68
+
69
+ - Verify IP is anonymized before persisting.
70
+ - Verify `user_agent` is truncated to 500 chars.
71
+ - Verify `referer` is truncated to 500 chars.
72
+ - Verify metadata is stored as expected.
73
+
74
+ **Why**
75
+
76
+ This logic exists in the controller and affects privacy and database safety, but is currently untested.
77
+
78
+ ## Priority 2: Cover dashboard and model behavior
79
+
80
+ ### Task 6: Add controller/integration tests for dashboard access
81
+
82
+ - Verify `/a/dashboard` resolves correctly.
83
+ - Verify dashboard renders when no auth proc is configured.
84
+ - Verify configured `authenticate_dashboard` proc is executed.
85
+ - Verify auth proc can redirect unauthenticated users.
86
+
87
+ **Why**
88
+
89
+ Dashboard access is a user-facing feature and currently has no behavioral coverage.
90
+
91
+ ### Task 7: Add dashboard stats tests
92
+
93
+ - Seed clicks across different timestamps.
94
+ - Verify:
95
+ - `total_clicks`
96
+ - `today_clicks`
97
+ - `week_clicks`
98
+ - `unique_destinations`
99
+ - recent clicks ordering
100
+ - top destinations counts
101
+
102
+ **Why**
103
+
104
+ The dashboard is mostly aggregation logic. That kind of logic regresses easily when queries change.
105
+
106
+ ### Task 8: Add model tests for `AffiliateTracker::Click`
107
+
108
+ - Test validations for `destination_url` and `clicked_at`.
109
+ - Test `domain` with:
110
+ - valid URL,
111
+ - invalid URL,
112
+ - URL without host if relevant.
113
+ - Test scopes:
114
+ - `.today`
115
+ - `.this_week`
116
+ - `.this_month`
117
+
118
+ **Why**
119
+
120
+ The model has public behavior that is not currently covered at all.
121
+
122
+ ## Priority 3: Fix test architecture issues
123
+
124
+ ### Task 9: Stop stubbing the gem's public API in `test/test_helper.rb`
125
+
126
+ - Load the real `lib/affiliate_tracker.rb` where possible.
127
+ - Remove duplicated test-only implementations of:
128
+ - `AffiliateTracker.configure`
129
+ - `AffiliateTracker.track_url`
130
+ - `AffiliateTracker.url`
131
+ - Keep only the minimal Rails test scaffolding needed by the gem.
132
+
133
+ **Why**
134
+
135
+ Current tests can pass even if the real entry point breaks, especially around `default_metadata`.
136
+
137
+ ### Task 10: Add tests for `default_metadata`
138
+
139
+ - Verify configured `default_metadata` is merged into generated tracking URLs.
140
+ - Verify explicit per-link metadata overrides default values where expected.
141
+ - Verify non-hash return value falls back to `{}`.
142
+ - Verify exceptions inside `default_metadata` do not break URL generation.
143
+
144
+ **Why**
145
+
146
+ This is real logic in the gem entry point and is currently untested because the suite bypasses it.
147
+
148
+ ## Priority 4: Replace low-value indirect tests
149
+
150
+ ### Task 11: Replace route file parsing tests with routing behavior tests
151
+
152
+ - Replace string-based assertions against `config/routes.rb`.
153
+ - Add routing tests that verify:
154
+ - `/a/dashboard` routes to dashboard controller,
155
+ - `/a/:payload` routes to click redirect,
156
+ - dashboard route is not swallowed by payload route.
157
+
158
+ **Why**
159
+
160
+ Parsing the routes file as text is brittle and does not prove the router behaves correctly.
161
+
162
+ ### Task 12: Remove or rewrite the Base64 `dashboard` test
163
+
164
+ - Delete or replace the test asserting `"dashboard"` is not valid Base64 payload.
165
+ - If kept, justify it with actual runtime behavior.
166
+
167
+ **Why**
168
+
169
+ That test does not meaningfully protect routing behavior.
170
+
171
+ ## Priority 5: Reduce redundant coverage
172
+
173
+ ### Task 13: Consolidate overlapping URL generation tests
174
+
175
+ - Review overlap between:
176
+ - `ConfigurationTest`
177
+ - `UrlGeneratorTest`
178
+ - `ViewHelpersTest`
179
+ - Keep one focused place for:
180
+ - signature format,
181
+ - `/a/` URL structure,
182
+ - same input => same output,
183
+ - different input => different signature.
184
+
185
+ **Why**
186
+
187
+ The suite repeats the same contract several times with little extra protection.
188
+
189
+ ### Task 14: Trim repetitive helper tests
190
+
191
+ - Keep tests that cover real helper-specific behavior:
192
+ - HTML escaping,
193
+ - block syntax,
194
+ - HTML attributes vs tracking metadata split,
195
+ - default `target` and `rel`,
196
+ - overriding `target` and `rel`.
197
+ - Reduce repeated cases that only re-prove `UrlGenerator.decode`.
198
+
199
+ **Why**
200
+
201
+ `ViewHelpersTest` is large, but much of it duplicates lower-level URL generator coverage.
202
+
203
+ ## Optional: Installer and engine coverage
204
+
205
+ ### Task 15: Add tests for engine helper inclusion
206
+
207
+ - Verify helpers are available in Action View.
208
+ - Verify helpers are available in Action Mailer if the gem promises that behavior.
209
+
210
+ ### Task 16: Add generator tests
211
+
212
+ - Verify install generator:
213
+ - creates initializer,
214
+ - creates migration,
215
+ - mounts engine route.
216
+
217
+ **Why**
218
+
219
+ For a gem, installation and framework integration are part of the product surface.
220
+
221
+ ## Suggested execution order
222
+
223
+ 1. Add redirect integration tests.
224
+ 2. Add deduplication and fallback tests.
225
+ 3. Add dashboard and model tests.
226
+ 4. Fix `test_helper` to use the real gem entry point.
227
+ 5. Replace indirect route tests.
228
+ 6. Remove redundant helper and URL tests.
229
+
230
+ ## Definition of done
231
+
232
+ - Main click flow is covered end-to-end.
233
+ - Failure paths are covered end-to-end.
234
+ - Public config hooks are covered.
235
+ - Dashboard behavior is covered.
236
+ - Model behavior is covered.
237
+ - Tests execute against real production entry points, not test-only rewrites.
238
+ - Redundant tests are reduced without losing regression protection.
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AffiliateTracker
4
+ class ApplicationController < ActionController::Base
5
+ protect_from_forgery with: :exception
6
+ end
7
+ end