consently 0.2.1 → 1.0.0
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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +51 -14
- data/app/helpers/consently/tags_helper.rb +70 -19
- data/app/javascript/consently/banner_controller.js +3 -0
- data/app/javascript/consently/events.js +78 -0
- data/config/importmap.rb +1 -0
- data/lib/consently/configuration.rb +16 -0
- data/lib/consently/engine.rb +1 -1
- data/lib/consently/event.rb +54 -0
- data/lib/consently/version.rb +1 -1
- data/lib/consently.rb +15 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dab77a672e8b422500eceb9e90f0481fc4f7abb180e22ca7c12fadf98c4ef9ef
|
|
4
|
+
data.tar.gz: 802a3a6d8c5915acce9d6e843bb5fd8b10a28a7eafb4dd68d0af8bc8617ba43b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3097f21d9037f63e8d27a697e73c2fae5e54b246e1de83e6722a40b2915005400c45ede835a7d8b77aaf1c3da55b877ba6318e23617aad7b3d00de9d0b001e59
|
|
7
|
+
data.tar.gz: 4247ab6d468b280bc7b784c52401d1f4ee53d98e627f168206159bea2daebc08ff3c359124afd71849b7cf535de37f9b50b49795e020099c988db74dfd12fc67
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0 (2026-09-04)
|
|
4
|
+
|
|
5
|
+
- Events speak the language of the page's tags: `gtag('event', ...)` under
|
|
6
|
+
gtag.js, a `dataLayer` push under Google Tag Manager, decided per request
|
|
7
|
+
from the tags in scope or forced with `event_transport`. Before, every event
|
|
8
|
+
was a dataLayer push - which gtag.js ignores, so a shop without a container
|
|
9
|
+
lost its whole ecommerce funnel.
|
|
10
|
+
- An event rendered without consent is held back as an inert script and
|
|
11
|
+
released with the tags when its category is granted, instead of being
|
|
12
|
+
dropped. The page a visitor accepts on now counts its `view_item`.
|
|
13
|
+
- `consently_stream_event` sends an event from a Turbo Stream response - the
|
|
14
|
+
add-to-cart that never renders a page - through a stream action that ships
|
|
15
|
+
with the banner controller; it is sent at once with consent and queued
|
|
16
|
+
until then otherwise.
|
|
17
|
+
- `Consently.track(event, payload, { category })` and `Consently.granted()`
|
|
18
|
+
in JavaScript, on the same queue.
|
|
19
|
+
- `consently_event` is the new name of `consently_data_layer_push`; the old
|
|
20
|
+
one still works.
|
|
21
|
+
|
|
3
22
|
## 0.2.1 (2026-08-13)
|
|
4
23
|
|
|
5
24
|
- The banner's custom properties moved from `.consently` to `:root`, so the
|
data/README.md
CHANGED
|
@@ -201,8 +201,9 @@ status panel - ask for a reload instead:
|
|
|
201
201
|
c.reload_after_choice = true
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
Either way
|
|
205
|
-
|
|
204
|
+
Either way the events waiting on that consent go out too (see below), and a
|
|
205
|
+
`consently:change` event fires on `document`, carrying the granted categories,
|
|
206
|
+
so you can react to it yourself:
|
|
206
207
|
|
|
207
208
|
```js
|
|
208
209
|
document.addEventListener("consently:change", ({ detail }) => {
|
|
@@ -237,7 +238,7 @@ button carries `aria-expanded` and `aria-controls`, reopening the panel moves
|
|
|
237
238
|
focus into it, and the animation gives way to `prefers-reduced-motion`. Nobody
|
|
238
239
|
is trapped in a focus cycle they did not ask for.
|
|
239
240
|
|
|
240
|
-
##
|
|
241
|
+
## Events
|
|
241
242
|
|
|
242
243
|
GA4 wants a particular shape, and your models are not it. Hand the helper
|
|
243
244
|
whatever you have:
|
|
@@ -249,17 +250,50 @@ whatever you have:
|
|
|
249
250
|
|
|
250
251
|
Items may be hashes already in GA4 shape, or any object answering to
|
|
251
252
|
`sku`/`id`, `name`, `price`, `quantity`, `category`, `brand`, `variant` - a
|
|
252
|
-
line item or a product usually does.
|
|
253
|
-
cleared first, as Google asks, so two events on one page cannot bleed into
|
|
254
|
-
each other.
|
|
253
|
+
line item or a product usually does. Anything else:
|
|
255
254
|
|
|
256
|
-
|
|
255
|
+
```erb
|
|
256
|
+
<%= consently_event "newsletter_signup", source: "footer" %>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**Sent the way your tags listen.** A page running gtag.js gets
|
|
260
|
+
`gtag('event', ...)` calls; a page running Google Tag Manager gets `dataLayer`
|
|
261
|
+
pushes, the previous `ecommerce` object cleared first as Google asks. The gem
|
|
262
|
+
decides per request from the tags in scope - a container means the dataLayer,
|
|
263
|
+
any other Google tag means gtag - because a push meant for a container is
|
|
264
|
+
something gtag.js silently ignores, and a whole checkout funnel can go missing
|
|
265
|
+
that way. Force it if you must:
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
c.event_transport = :data_layer # or :gtag; :auto is the default
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**Held back, not dropped.** Without consent an event is rendered the way a tag
|
|
272
|
+
is - an inert `<script type="text/plain">` - and released together with the
|
|
273
|
+
tags the moment its category is granted. The product page a visitor accepts
|
|
274
|
+
on still counts its `view_item`.
|
|
275
|
+
|
|
276
|
+
**From a Turbo Stream.** An add-to-cart that answers with a stream renders no
|
|
277
|
+
page to put a script on. The stream carries the event instead, and the
|
|
278
|
+
JavaScript side sends it at once if the category is granted, or keeps it until
|
|
279
|
+
it is:
|
|
257
280
|
|
|
258
281
|
```erb
|
|
259
|
-
|
|
282
|
+
<%# line_items/create.turbo_stream.erb %>
|
|
283
|
+
<%= turbo_stream.replace "cart", partial: "cart" %>
|
|
284
|
+
<%= consently_stream_event "add_to_cart", items: [@line_item],
|
|
285
|
+
currency: "EUR", value: @line_item.price %>
|
|
260
286
|
```
|
|
261
287
|
|
|
262
|
-
|
|
288
|
+
Nothing to register: the action arrives with the banner controller.
|
|
289
|
+
|
|
290
|
+
**From your own JavaScript.** The same queue, the same rule:
|
|
291
|
+
|
|
292
|
+
```js
|
|
293
|
+
Consently.track("newsletter_signup", { source: "footer" })
|
|
294
|
+
Consently.track("lead", { value: 1 }, { category: "marketing", ecommerce: false })
|
|
295
|
+
Consently.granted("analytics") // true or false, right now
|
|
296
|
+
```
|
|
263
297
|
|
|
264
298
|
## Embedded videos and maps
|
|
265
299
|
|
|
@@ -376,13 +410,14 @@ Helpers:
|
|
|
376
410
|
|
|
377
411
|
| | |
|
|
378
412
|
| --- | --- |
|
|
379
|
-
| `consently_tags` | `<head>`: the stylesheet, consent mode defaults,
|
|
413
|
+
| `consently_tags` | `<head>`: the stylesheet, consent mode defaults, every tag (blocked or live), and what the JavaScript side needs to know |
|
|
380
414
|
| `consently_noscript_tags` | after `<body>`: GTM and Meta fallbacks, for granted categories only |
|
|
381
415
|
| `consently_banner` | the banner, the panel, and the JavaScript that releases blocked tags |
|
|
382
416
|
| `consently_policy` | the generated cookie policy: categories, vendors, cookies, durations |
|
|
383
417
|
| `consently_preferences_link` | "Cookie settings" link; anything with `data-consently-open` reopens the panel |
|
|
384
|
-
| `consently_ecommerce(event, items:, **params)` | a GA4 ecommerce event, items mapped from your own objects |
|
|
385
|
-
| `
|
|
418
|
+
| `consently_ecommerce(event, items:, **params)` | a GA4 ecommerce event, items mapped from your own objects; held back until consent |
|
|
419
|
+
| `consently_event(event, **payload)` | any other event, gtag or dataLayer as the page's tags expect; held back until consent |
|
|
420
|
+
| `consently_stream_event(event, items:, **payload)` | the same from a Turbo Stream response, sent or queued in the browser |
|
|
386
421
|
| `consently_embed(kind, id, category:, ratio:)` | a video or map that waits for consent |
|
|
387
422
|
| `consently_consent` | the current `Consently::Consent`; `granted?(:analytics)` in your own views |
|
|
388
423
|
|
|
@@ -397,6 +432,7 @@ Configuration:
|
|
|
397
432
|
| `c.enabled` | `true`, `false`, or a callable taking the request |
|
|
398
433
|
| `c.reload_after_choice` | reload once a choice is made; off by default |
|
|
399
434
|
| `c.google_consent_mode` | `:basic` (default), `:advanced`, or `false` |
|
|
435
|
+
| `c.event_transport` | `:auto` (default: gtag under gtag.js, the dataLayer under a container), `:gtag`, `:data_layer` |
|
|
400
436
|
| `c.log_consents`, `c.consent_subject` | store proof of each decision, optionally naming who |
|
|
401
437
|
| `c.stylesheet` | link the banner's CSS; off if you style it yourself |
|
|
402
438
|
| `c.cookie_name`, `c.cookie_max_age`, `c.cookie_path`, `c.cookie_domain` | where the choice is kept |
|
|
@@ -404,8 +440,9 @@ Configuration:
|
|
|
404
440
|
| `c.respect_do_not_track`, `c.respect_global_privacy_control` | treat an opt-out signal as a rejection |
|
|
405
441
|
| `c.consent_required` | who has to be asked at all; false means no banner and everything granted |
|
|
406
442
|
|
|
407
|
-
JavaScript:
|
|
408
|
-
|
|
443
|
+
JavaScript: `Consently.track(event, payload, { category })` sends or queues
|
|
444
|
+
an event, `Consently.granted(category)` says where things stand, and a
|
|
445
|
+
`consently:change` event fires on `document` with the granted categories.
|
|
409
446
|
|
|
410
447
|
## Licence
|
|
411
448
|
|
|
@@ -9,7 +9,7 @@ module Consently
|
|
|
9
9
|
def consently_tags
|
|
10
10
|
return "".html_safe unless consently_enabled?
|
|
11
11
|
|
|
12
|
-
parts = []
|
|
12
|
+
parts = [ consently_state_tags ]
|
|
13
13
|
parts << consently_stylesheet_tag if Consently.config.stylesheet
|
|
14
14
|
parts << consently_consent_mode_tag if Consently.config.google_consent_mode
|
|
15
15
|
Consently.tags_for(request).each do |provider|
|
|
@@ -75,17 +75,21 @@ module Consently
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
78
|
+
# An analytics event from a view, sent the way this page's tags expect -
|
|
79
|
+
# gtag('event', ...) under gtag.js, a dataLayer push under Google Tag
|
|
80
|
+
# Manager (see Consently.event_transport_for) - and only with consent.
|
|
81
|
+
# Without it the event is held back the way a tag is: an inert script
|
|
82
|
+
# the banner releases the moment the category is granted, so the page a
|
|
83
|
+
# visitor accepts on still counts.
|
|
80
84
|
#
|
|
81
|
-
# <%=
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
payload = payload.merge(event: event)
|
|
86
|
-
consently_inline_script "window.dataLayer = window.dataLayer || []; window.dataLayer.push(#{payload.to_json});"
|
|
85
|
+
# <%= consently_event("newsletter_signup", source: "footer") %>
|
|
86
|
+
def consently_event(event, category: :analytics, **payload)
|
|
87
|
+
consently_event_tag(Event.new(event, payload), category)
|
|
87
88
|
end
|
|
88
89
|
|
|
90
|
+
# The name this helper had before it learnt to speak gtag.
|
|
91
|
+
alias_method :consently_data_layer_push, :consently_event
|
|
92
|
+
|
|
89
93
|
# A GA4 ecommerce event in the shape Google expects, from whatever your
|
|
90
94
|
# models happen to look like:
|
|
91
95
|
#
|
|
@@ -96,19 +100,32 @@ module Consently
|
|
|
96
100
|
# few obvious names (id/sku, name, price, quantity, category, brand,
|
|
97
101
|
# variant) - a LineItem or a Product usually does.
|
|
98
102
|
#
|
|
99
|
-
#
|
|
100
|
-
# events on one page cannot bleed into
|
|
103
|
+
# Under Google Tag Manager the previous ecommerce object is cleared
|
|
104
|
+
# first, as Google asks, so two events on one page cannot bleed into
|
|
105
|
+
# each other. Held back without consent, like consently_event.
|
|
101
106
|
def consently_ecommerce(event, items: [], category: :analytics, **params)
|
|
102
|
-
|
|
107
|
+
consently_event_tag(Event.new(event, params.merge(items: consently_ecommerce_items(items)), ecommerce: true), category)
|
|
108
|
+
end
|
|
103
109
|
|
|
104
|
-
|
|
105
|
-
|
|
110
|
+
# The same event from a Turbo Stream response - an add-to-cart that
|
|
111
|
+
# never renders a page. Items make it an ecommerce event. The stream
|
|
112
|
+
# carries the event, not a script: the JavaScript side sends it at once
|
|
113
|
+
# if the category is granted and keeps it until then otherwise, so a
|
|
114
|
+
# cart filled before the click is counted after it.
|
|
115
|
+
#
|
|
116
|
+
# <%= consently_stream_event("add_to_cart", items: [ @line_item ], currency: "EUR", value: 12.5) %>
|
|
117
|
+
def consently_stream_event(event, items: nil, category: :analytics, **payload)
|
|
118
|
+
return "".html_safe unless consently_enabled?
|
|
106
119
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
payload = payload.merge(items: consently_ecommerce_items(items)) unless items.nil?
|
|
121
|
+
event = Event.new(event, payload, ecommerce: !items.nil?)
|
|
122
|
+
|
|
123
|
+
content_tag("turbo-stream", "",
|
|
124
|
+
action: "consently_event",
|
|
125
|
+
event: event.name,
|
|
126
|
+
category: category,
|
|
127
|
+
ecommerce: event.ecommerce?,
|
|
128
|
+
payload: event.payload.to_json)
|
|
112
129
|
end
|
|
113
130
|
|
|
114
131
|
# A complete cookie policy for the tags this request would load: every
|
|
@@ -167,6 +184,40 @@ module Consently
|
|
|
167
184
|
Consently.enabled?(request)
|
|
168
185
|
end
|
|
169
186
|
|
|
187
|
+
# Two <meta> tags the JavaScript side reads: which categories this page
|
|
188
|
+
# was rendered with, and how events are to be sent. Provisional head
|
|
189
|
+
# elements, so Turbo Drive swaps them on every visit.
|
|
190
|
+
def consently_state_tags
|
|
191
|
+
granted = Consently.config.categories.select { |category| consently_consent.granted?(category) }
|
|
192
|
+
|
|
193
|
+
safe_join([
|
|
194
|
+
tag.meta(name: "consently-granted", content: granted.join(" ")),
|
|
195
|
+
tag.meta(name: "consently-transport", content: consently_event_transport)
|
|
196
|
+
], "\n")
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def consently_event_transport
|
|
200
|
+
@consently_event_transport ||= Consently.event_transport_for(request)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Live when the category is granted; otherwise inert, and released by the
|
|
204
|
+
# banner together with the tags the moment it is.
|
|
205
|
+
def consently_event_tag(event, category)
|
|
206
|
+
return "".html_safe unless consently_enabled?
|
|
207
|
+
|
|
208
|
+
javascript = event.to_js(consently_event_transport)
|
|
209
|
+
return consently_inline_script(javascript) if consently_consent.granted?(category)
|
|
210
|
+
|
|
211
|
+
attributes = { type: "text/plain", data: { "consently-category" => category } }
|
|
212
|
+
attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
|
|
213
|
+
|
|
214
|
+
content_tag(:script, javascript.html_safe, attributes)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def consently_ecommerce_items(items)
|
|
218
|
+
Array(items).map { |item| consently_ecommerce_item(item) }
|
|
219
|
+
end
|
|
220
|
+
|
|
170
221
|
# Whether this tag may run now. Normally that means consent; under
|
|
171
222
|
# advanced consent mode Google's own tags also load before it, having
|
|
172
223
|
# been told to store nothing until it arrives.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
// Loaded for its side effects: the event queue, the Turbo Stream action and
|
|
3
|
+
// window.Consently.track come along with the banner.
|
|
4
|
+
import "consently/events"
|
|
2
5
|
|
|
3
6
|
// The banner, the preferences panel, and the part that actually matters:
|
|
4
7
|
// turning the blocked <script type="text/plain"> tags into live ones the
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Analytics events that wait for consent.
|
|
2
|
+
//
|
|
3
|
+
// Three roads lead here. An event rendered into a page without consent is an
|
|
4
|
+
// inert <script> the banner releases together with the tags - nothing to do
|
|
5
|
+
// on this side. A <turbo-stream action="consently_event"> comes from a
|
|
6
|
+
// response that renders no page, and window.Consently.track() from your own
|
|
7
|
+
// code; those two are sent at once when their category is granted and kept
|
|
8
|
+
// until it is otherwise, so an add-to-cart that happened before the click is
|
|
9
|
+
// counted after it.
|
|
10
|
+
//
|
|
11
|
+
// The page says what is granted and how to send in two <meta> tags written by
|
|
12
|
+
// consently_tags. The banner's consently:change event overrides the first the
|
|
13
|
+
// moment a choice is made, and the next Turbo visit brings a fresh page whose
|
|
14
|
+
// tags already know.
|
|
15
|
+
|
|
16
|
+
const NECESSARY = "necessary"
|
|
17
|
+
const waiting = []
|
|
18
|
+
let decided = null
|
|
19
|
+
|
|
20
|
+
export function track(name, payload = {}, { category = "analytics", ecommerce = false } = {}) {
|
|
21
|
+
const event = { name, payload, category, ecommerce }
|
|
22
|
+
|
|
23
|
+
if (granted(category)) send(event)
|
|
24
|
+
else waiting.push(event)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function granted(category) {
|
|
28
|
+
if (category === NECESSARY) return true
|
|
29
|
+
|
|
30
|
+
return (decided ?? meta("consently-granted").split(" ")).includes(category)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function meta(name) {
|
|
34
|
+
return document.querySelector(`meta[name="${name}"]`)?.content ?? ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function send({ name, payload, ecommerce }) {
|
|
38
|
+
window.dataLayer = window.dataLayer || []
|
|
39
|
+
|
|
40
|
+
if (meta("consently-transport") === "gtag") {
|
|
41
|
+
// gtag.js reads the queue, so this is safe before the tag has loaded.
|
|
42
|
+
window.gtag = window.gtag || function () { window.dataLayer.push(arguments) }
|
|
43
|
+
window.gtag("event", name, payload)
|
|
44
|
+
} else if (ecommerce) {
|
|
45
|
+
window.dataLayer.push({ ecommerce: null })
|
|
46
|
+
window.dataLayer.push({ event: name, ecommerce: payload })
|
|
47
|
+
} else {
|
|
48
|
+
window.dataLayer.push({ event: name, ...payload })
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function release(categories) {
|
|
53
|
+
decided = [...categories, NECESSARY]
|
|
54
|
+
|
|
55
|
+
waiting.splice(0).forEach((event) => (granted(event.category) ? send(event) : waiting.push(event)))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
document.addEventListener("consently:change", (event) => release(event.detail.categories))
|
|
59
|
+
|
|
60
|
+
// A new page carries the server's reading of the cookie, which also knows
|
|
61
|
+
// about policy versions and expiry; from here on it is the one to trust.
|
|
62
|
+
document.addEventListener("turbo:load", () => { decided = null })
|
|
63
|
+
|
|
64
|
+
// The Turbo Stream action. A listener rather than an entry in
|
|
65
|
+
// Turbo.StreamActions, so the gem imports nothing from Turbo and the order
|
|
66
|
+
// the modules load in does not matter.
|
|
67
|
+
document.addEventListener("turbo:before-stream-render", (event) => {
|
|
68
|
+
const stream = event.target
|
|
69
|
+
if (stream.getAttribute("action") !== "consently_event") return
|
|
70
|
+
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
track(stream.getAttribute("event"), JSON.parse(stream.getAttribute("payload") || "{}"), {
|
|
73
|
+
category: stream.getAttribute("category") || "analytics",
|
|
74
|
+
ecommerce: stream.getAttribute("ecommerce") === "true"
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
window.Consently = Object.assign(window.Consently || {}, { track, granted })
|
data/config/importmap.rb
CHANGED
|
@@ -2,3 +2,4 @@
|
|
|
2
2
|
# resolves without anyone editing config/importmap.rb.
|
|
3
3
|
pin "consently/banner_controller", to: "consently/banner_controller.js", preload: true
|
|
4
4
|
pin "consently/embed_controller", to: "consently/embed_controller.js", preload: true
|
|
5
|
+
pin "consently/events", to: "consently/events.js", preload: true
|
|
@@ -50,6 +50,21 @@ module Consently
|
|
|
50
50
|
google_consent_mode == :advanced
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# How events are sent: :gtag for a page running gtag.js, :data_layer for
|
|
54
|
+
# one running Google Tag Manager. The default, :auto, decides per request
|
|
55
|
+
# from the tags in its scope (see Consently.event_transport_for) and is
|
|
56
|
+
# right unless you push to a container the gem does not know about.
|
|
57
|
+
attr_reader :event_transport
|
|
58
|
+
|
|
59
|
+
def event_transport=(transport)
|
|
60
|
+
transport = transport&.to_sym
|
|
61
|
+
unless [ :auto, *Event::TRANSPORTS ].include?(transport)
|
|
62
|
+
raise ArgumentError, "event_transport must be :auto, :gtag or :data_layer"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
@event_transport = transport
|
|
66
|
+
end
|
|
67
|
+
|
|
53
68
|
# Store a row per decision, as proof of consent. Needs the engine mounted
|
|
54
69
|
# and the migration from `rails g consently:consent_log`.
|
|
55
70
|
attr_accessor :log_consents
|
|
@@ -109,6 +124,7 @@ module Consently
|
|
|
109
124
|
@consent_version = 1
|
|
110
125
|
@enabled = true
|
|
111
126
|
@google_consent_mode = :basic
|
|
127
|
+
@event_transport = :auto
|
|
112
128
|
@stylesheet = true
|
|
113
129
|
@log_consents = false
|
|
114
130
|
@respect_do_not_track = false
|
data/lib/consently/engine.rb
CHANGED
|
@@ -14,7 +14,7 @@ module Consently
|
|
|
14
14
|
if app.config.respond_to?(:assets)
|
|
15
15
|
app.config.assets.paths << root.join("app/javascript")
|
|
16
16
|
if app.config.assets.respond_to?(:precompile)
|
|
17
|
-
app.config.assets.precompile += %w[consently.css consently/banner_controller.js consently/embed_controller.js]
|
|
17
|
+
app.config.assets.precompile += %w[consently.css consently/banner_controller.js consently/embed_controller.js consently/events.js]
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "active_support/core_ext/object/json"
|
|
2
|
+
|
|
3
|
+
module Consently
|
|
4
|
+
# One analytics event on its way to the page: a name, a payload, and whether
|
|
5
|
+
# it is a GA4 ecommerce event. It is written as JavaScript for whichever
|
|
6
|
+
# transport the page runs on - gtag('event', ...) for gtag.js, a dataLayer
|
|
7
|
+
# push for Google Tag Manager - so the helpers never have to know which.
|
|
8
|
+
class Event
|
|
9
|
+
TRANSPORTS = %i[gtag data_layer].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :name, :payload
|
|
12
|
+
|
|
13
|
+
def initialize(name, payload = {}, ecommerce: false)
|
|
14
|
+
@name = name.to_s
|
|
15
|
+
@payload = payload.to_h.compact
|
|
16
|
+
@ecommerce = ecommerce
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ecommerce?
|
|
20
|
+
@ecommerce
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_js(transport)
|
|
24
|
+
case transport.to_sym
|
|
25
|
+
when :gtag then gtag_js
|
|
26
|
+
when :data_layer then data_layer_js
|
|
27
|
+
else raise ArgumentError, "Consently: unknown event transport #{transport.inspect}, expected one of #{TRANSPORTS.join(", ")}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# gtag.js reads the dataLayer as a queue, so an event pushed before the
|
|
34
|
+
# tag has loaded is not lost. That is what lets a released event fire the
|
|
35
|
+
# moment consent arrives, while gtag.js is still on its way.
|
|
36
|
+
def gtag_js
|
|
37
|
+
"window.dataLayer = window.dataLayer || []; " \
|
|
38
|
+
"window.gtag = window.gtag || function(){dataLayer.push(arguments);}; " \
|
|
39
|
+
"gtag('event', #{name.to_json}, #{payload.to_json});"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def data_layer_js
|
|
43
|
+
if ecommerce?
|
|
44
|
+
# Cleared first, as Google asks, so two events on one page cannot
|
|
45
|
+
# bleed into each other.
|
|
46
|
+
"window.dataLayer = window.dataLayer || []; " \
|
|
47
|
+
"window.dataLayer.push({ ecommerce: null }); " \
|
|
48
|
+
"window.dataLayer.push(#{{ event: name, ecommerce: payload }.to_json});"
|
|
49
|
+
else
|
|
50
|
+
"window.dataLayer = window.dataLayer || []; window.dataLayer.push(#{payload.merge(event: name).to_json});"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/consently/version.rb
CHANGED
data/lib/consently.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "consently/version"
|
|
|
7
7
|
require "consently/script"
|
|
8
8
|
require "consently/cookie"
|
|
9
9
|
require "consently/consent"
|
|
10
|
+
require "consently/event"
|
|
10
11
|
require "consently/providers/base"
|
|
11
12
|
require "consently/providers"
|
|
12
13
|
require "consently/configuration"
|
|
@@ -60,6 +61,20 @@ module Consently
|
|
|
60
61
|
resolve(config.enabled, request)
|
|
61
62
|
end
|
|
62
63
|
|
|
64
|
+
# How events reach Google for this request. Google Tag Manager reads
|
|
65
|
+
# dataLayer pushes; gtag.js only acts on gtag('event', ...) calls, and a
|
|
66
|
+
# push meant for a container it silently ignores. :auto looks at the tags
|
|
67
|
+
# in the request's scope: a container present means the dataLayer,
|
|
68
|
+
# otherwise any Google tag means gtag.
|
|
69
|
+
def event_transport_for(request = nil)
|
|
70
|
+
return config.event_transport unless config.event_transport == :auto
|
|
71
|
+
|
|
72
|
+
providers = tags_for(request)
|
|
73
|
+
return :data_layer if providers.any? { |provider| provider.is_a?(Providers::GoogleTagManager) }
|
|
74
|
+
|
|
75
|
+
providers.any?(&:google?) ? :gtag : :data_layer
|
|
76
|
+
end
|
|
77
|
+
|
|
63
78
|
# Whether this visitor has to be asked. When they do not, every category
|
|
64
79
|
# counts as granted and no banner is rendered - see config.consent_required.
|
|
65
80
|
def consent_required?(request = nil)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: consently
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michał Krzysteczko
|
|
@@ -58,6 +58,7 @@ files:
|
|
|
58
58
|
- app/helpers/consently/tags_helper.rb
|
|
59
59
|
- app/javascript/consently/banner_controller.js
|
|
60
60
|
- app/javascript/consently/embed_controller.js
|
|
61
|
+
- app/javascript/consently/events.js
|
|
61
62
|
- app/models/consently/application_record.rb
|
|
62
63
|
- app/models/consently/consent_record.rb
|
|
63
64
|
- app/views/consently/_banner.html.erb
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- lib/consently/consent.rb
|
|
81
82
|
- lib/consently/cookie.rb
|
|
82
83
|
- lib/consently/engine.rb
|
|
84
|
+
- lib/consently/event.rb
|
|
83
85
|
- lib/consently/providers.rb
|
|
84
86
|
- lib/consently/providers/base.rb
|
|
85
87
|
- lib/consently/providers/clarity.rb
|
|
@@ -121,7 +123,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
121
123
|
- !ruby/object:Gem::Version
|
|
122
124
|
version: '0'
|
|
123
125
|
requirements: []
|
|
124
|
-
rubygems_version: 4.0.
|
|
126
|
+
rubygems_version: 4.0.18
|
|
125
127
|
specification_version: 4
|
|
126
128
|
summary: GDPR cookie consent for Rails that actually blocks your analytics tags until
|
|
127
129
|
the visitor agrees
|