consently 0.2.1 → 2.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 +38 -0
- data/README.md +70 -18
- data/app/assets/stylesheets/consently.css +40 -3
- data/app/helpers/consently/tags_helper.rb +79 -19
- data/app/javascript/consently/banner_controller.js +3 -0
- data/app/javascript/consently/events.js +78 -0
- data/app/views/consently/_banner.html.erb +3 -1
- data/config/importmap.rb +1 -0
- data/config/locales/cs.yml +2 -0
- data/config/locales/de.yml +2 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/es.yml +2 -0
- data/config/locales/fr.yml +2 -0
- data/config/locales/hu.yml +2 -0
- data/config/locales/it.yml +2 -0
- data/config/locales/nl.yml +2 -0
- data/config/locales/pl.yml +2 -0
- data/config/locales/sk.yml +2 -0
- data/lib/consently/configuration.rb +56 -7
- 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
- data/lib/generators/consently/install/templates/consently.rb +3 -1
- 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: 5a4c3433bcb18de24b5fc9849e955ef217502f51f01e4a281e23a493ae18a681
|
|
4
|
+
data.tar.gz: 1347eb53ce32770929522ada6c86a35a068c450cd738466d88913f98019191c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 532e7e0eab42de4a040fedb640ef70cb575cd651d3b871c743cd3259abf9562f681248c25b1d9d2662321c7d904850544404072ac3da75316cd7fa31962b8a3c
|
|
7
|
+
data.tar.gz: 2b4cbaa8b217c5691836b8b425d716e862642f865018a7dc24da428c9eb3861d35bc3a475238a56ae7857df3038e32462e33b3f7bafb9673dc53af727eebad29
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.0 (2026-10-31)
|
|
4
|
+
|
|
5
|
+
- The banner asks only about categories in use: one with a tag in it (in any
|
|
6
|
+
scope) or one declared with `c.category`. A site with nothing but Google
|
|
7
|
+
Analytics no longer shows a "Marketing" checkbox for marketing it does not
|
|
8
|
+
do. `c.categories = [...]` fixes the list by hand.
|
|
9
|
+
- The banner message follows the categories: analytics alone reads "to
|
|
10
|
+
understand how it is used and improve it", without the "relevant offers"
|
|
11
|
+
that only marketing justifies. Every locale has the new `message_analytics`
|
|
12
|
+
and `message_marketing`.
|
|
13
|
+
- No optional category, no banner: there is nothing to ask.
|
|
14
|
+
- Checkboxes are drawn by the gem's CSS instead of left native, so a reset in
|
|
15
|
+
the host app (`@tailwindcss/forms` turns every checkbox square) no longer
|
|
16
|
+
restyles them. `--consently-checkbox-border` and
|
|
17
|
+
`--consently-checkbox-radius` tune them.
|
|
18
|
+
- **Upgrading:** `consently_embed` raises when its category is not offered,
|
|
19
|
+
since the visitor could never release it. A site whose embeds wait for
|
|
20
|
+
`marketing` without any marketing tag adds `c.category :marketing`.
|
|
21
|
+
|
|
22
|
+
## 1.0.0 (2026-09-04)
|
|
23
|
+
|
|
24
|
+
- Events speak the language of the page's tags: `gtag('event', ...)` under
|
|
25
|
+
gtag.js, a `dataLayer` push under Google Tag Manager, decided per request
|
|
26
|
+
from the tags in scope or forced with `event_transport`. Before, every event
|
|
27
|
+
was a dataLayer push - which gtag.js ignores, so a shop without a container
|
|
28
|
+
lost its whole ecommerce funnel.
|
|
29
|
+
- An event rendered without consent is held back as an inert script and
|
|
30
|
+
released with the tags when its category is granted, instead of being
|
|
31
|
+
dropped. The page a visitor accepts on now counts its `view_item`.
|
|
32
|
+
- `consently_stream_event` sends an event from a Turbo Stream response - the
|
|
33
|
+
add-to-cart that never renders a page - through a stream action that ships
|
|
34
|
+
with the banner controller; it is sent at once with consent and queued
|
|
35
|
+
until then otherwise.
|
|
36
|
+
- `Consently.track(event, payload, { category })` and `Consently.granted()`
|
|
37
|
+
in JavaScript, on the same queue.
|
|
38
|
+
- `consently_event` is the new name of `consently_data_layer_push`; the old
|
|
39
|
+
one still works.
|
|
40
|
+
|
|
3
41
|
## 0.2.1 (2026-08-13)
|
|
4
42
|
|
|
5
43
|
- The banner's custom properties moved from `.consently` to `:root`, so the
|
data/README.md
CHANGED
|
@@ -102,11 +102,25 @@ c.tag :custom, as: :inline_thing, category: :marketing, inline: "console.log('hi
|
|
|
102
102
|
|
|
103
103
|
## Consent
|
|
104
104
|
|
|
105
|
-
Three categories out of the box - `necessary`, `analytics`, `marketing` -
|
|
106
|
-
you
|
|
105
|
+
Three categories out of the box - `necessary`, `analytics`, `marketing` - but
|
|
106
|
+
the banner only asks about the ones you use: a category is offered when a tag
|
|
107
|
+
sits in it (in any scope) or when you declare it. A site running nothing but
|
|
108
|
+
Google Analytics gets a single checkbox, and a message that does not promise
|
|
109
|
+
"relevant offers" it never makes. With only `necessary` tags there is nothing
|
|
110
|
+
to ask, and no banner.
|
|
111
|
+
|
|
112
|
+
Declare a category that only your views use - an embed, an event - or one of
|
|
113
|
+
your own:
|
|
114
|
+
|
|
115
|
+
```ruby
|
|
116
|
+
c.category :marketing # a YouTube embed waits for it
|
|
117
|
+
c.category :personalization # name it in your locale file
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Or fix the list and its order outright (`necessary` is always added):
|
|
107
121
|
|
|
108
122
|
```ruby
|
|
109
|
-
c.
|
|
123
|
+
c.categories = %i[analytics marketing]
|
|
110
124
|
```
|
|
111
125
|
|
|
112
126
|
`necessary` never waits for a click. Everything else is blocked until the
|
|
@@ -201,8 +215,9 @@ status panel - ask for a reload instead:
|
|
|
201
215
|
c.reload_after_choice = true
|
|
202
216
|
```
|
|
203
217
|
|
|
204
|
-
Either way
|
|
205
|
-
|
|
218
|
+
Either way the events waiting on that consent go out too (see below), and a
|
|
219
|
+
`consently:change` event fires on `document`, carrying the granted categories,
|
|
220
|
+
so you can react to it yourself:
|
|
206
221
|
|
|
207
222
|
```js
|
|
208
223
|
document.addEventListener("consently:change", ({ detail }) => {
|
|
@@ -237,7 +252,7 @@ button carries `aria-expanded` and `aria-controls`, reopening the panel moves
|
|
|
237
252
|
focus into it, and the animation gives way to `prefers-reduced-motion`. Nobody
|
|
238
253
|
is trapped in a focus cycle they did not ask for.
|
|
239
254
|
|
|
240
|
-
##
|
|
255
|
+
## Events
|
|
241
256
|
|
|
242
257
|
GA4 wants a particular shape, and your models are not it. Hand the helper
|
|
243
258
|
whatever you have:
|
|
@@ -249,17 +264,50 @@ whatever you have:
|
|
|
249
264
|
|
|
250
265
|
Items may be hashes already in GA4 shape, or any object answering to
|
|
251
266
|
`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.
|
|
267
|
+
line item or a product usually does. Anything else:
|
|
255
268
|
|
|
256
|
-
|
|
269
|
+
```erb
|
|
270
|
+
<%= consently_event "newsletter_signup", source: "footer" %>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Sent the way your tags listen.** A page running gtag.js gets
|
|
274
|
+
`gtag('event', ...)` calls; a page running Google Tag Manager gets `dataLayer`
|
|
275
|
+
pushes, the previous `ecommerce` object cleared first as Google asks. The gem
|
|
276
|
+
decides per request from the tags in scope - a container means the dataLayer,
|
|
277
|
+
any other Google tag means gtag - because a push meant for a container is
|
|
278
|
+
something gtag.js silently ignores, and a whole checkout funnel can go missing
|
|
279
|
+
that way. Force it if you must:
|
|
280
|
+
|
|
281
|
+
```ruby
|
|
282
|
+
c.event_transport = :data_layer # or :gtag; :auto is the default
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Held back, not dropped.** Without consent an event is rendered the way a tag
|
|
286
|
+
is - an inert `<script type="text/plain">` - and released together with the
|
|
287
|
+
tags the moment its category is granted. The product page a visitor accepts
|
|
288
|
+
on still counts its `view_item`.
|
|
289
|
+
|
|
290
|
+
**From a Turbo Stream.** An add-to-cart that answers with a stream renders no
|
|
291
|
+
page to put a script on. The stream carries the event instead, and the
|
|
292
|
+
JavaScript side sends it at once if the category is granted, or keeps it until
|
|
293
|
+
it is:
|
|
257
294
|
|
|
258
295
|
```erb
|
|
259
|
-
|
|
296
|
+
<%# line_items/create.turbo_stream.erb %>
|
|
297
|
+
<%= turbo_stream.replace "cart", partial: "cart" %>
|
|
298
|
+
<%= consently_stream_event "add_to_cart", items: [@line_item],
|
|
299
|
+
currency: "EUR", value: @line_item.price %>
|
|
260
300
|
```
|
|
261
301
|
|
|
262
|
-
|
|
302
|
+
Nothing to register: the action arrives with the banner controller.
|
|
303
|
+
|
|
304
|
+
**From your own JavaScript.** The same queue, the same rule:
|
|
305
|
+
|
|
306
|
+
```js
|
|
307
|
+
Consently.track("newsletter_signup", { source: "footer" })
|
|
308
|
+
Consently.track("lead", { value: 1 }, { category: "marketing", ecommerce: false })
|
|
309
|
+
Consently.granted("analytics") // true or false, right now
|
|
310
|
+
```
|
|
263
311
|
|
|
264
312
|
## Embedded videos and maps
|
|
265
313
|
|
|
@@ -376,13 +424,14 @@ Helpers:
|
|
|
376
424
|
|
|
377
425
|
| | |
|
|
378
426
|
| --- | --- |
|
|
379
|
-
| `consently_tags` | `<head>`: the stylesheet, consent mode defaults,
|
|
427
|
+
| `consently_tags` | `<head>`: the stylesheet, consent mode defaults, every tag (blocked or live), and what the JavaScript side needs to know |
|
|
380
428
|
| `consently_noscript_tags` | after `<body>`: GTM and Meta fallbacks, for granted categories only |
|
|
381
429
|
| `consently_banner` | the banner, the panel, and the JavaScript that releases blocked tags |
|
|
382
430
|
| `consently_policy` | the generated cookie policy: categories, vendors, cookies, durations |
|
|
383
431
|
| `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
|
-
| `
|
|
432
|
+
| `consently_ecommerce(event, items:, **params)` | a GA4 ecommerce event, items mapped from your own objects; held back until consent |
|
|
433
|
+
| `consently_event(event, **payload)` | any other event, gtag or dataLayer as the page's tags expect; held back until consent |
|
|
434
|
+
| `consently_stream_event(event, items:, **payload)` | the same from a Turbo Stream response, sent or queued in the browser |
|
|
386
435
|
| `consently_embed(kind, id, category:, ratio:)` | a video or map that waits for consent |
|
|
387
436
|
| `consently_consent` | the current `Consently::Consent`; `granted?(:analytics)` in your own views |
|
|
388
437
|
|
|
@@ -392,11 +441,13 @@ Configuration:
|
|
|
392
441
|
| --- | --- |
|
|
393
442
|
| `c.tag key, **options` | `id:`, `domain:`, `category:`, `as:` (name it, for two of a kind) |
|
|
394
443
|
| `c.scope(name) { \|s\| ... }` | tags for one tenant; `c.scope_resolver` decides which one a request is |
|
|
395
|
-
| `c.category :name` | a category
|
|
444
|
+
| `c.category :name` | offer a category no tag uses: your own, or a built-in one for embeds and events |
|
|
445
|
+
| `c.categories = [...]` | fix the offered categories instead of deriving them from the tags |
|
|
396
446
|
| `c.consent_version` | bump it and every older consent stops counting |
|
|
397
447
|
| `c.enabled` | `true`, `false`, or a callable taking the request |
|
|
398
448
|
| `c.reload_after_choice` | reload once a choice is made; off by default |
|
|
399
449
|
| `c.google_consent_mode` | `:basic` (default), `:advanced`, or `false` |
|
|
450
|
+
| `c.event_transport` | `:auto` (default: gtag under gtag.js, the dataLayer under a container), `:gtag`, `:data_layer` |
|
|
400
451
|
| `c.log_consents`, `c.consent_subject` | store proof of each decision, optionally naming who |
|
|
401
452
|
| `c.stylesheet` | link the banner's CSS; off if you style it yourself |
|
|
402
453
|
| `c.cookie_name`, `c.cookie_max_age`, `c.cookie_path`, `c.cookie_domain` | where the choice is kept |
|
|
@@ -404,8 +455,9 @@ Configuration:
|
|
|
404
455
|
| `c.respect_do_not_track`, `c.respect_global_privacy_control` | treat an opt-out signal as a rejection |
|
|
405
456
|
| `c.consent_required` | who has to be asked at all; false means no banner and everything granted |
|
|
406
457
|
|
|
407
|
-
JavaScript:
|
|
408
|
-
|
|
458
|
+
JavaScript: `Consently.track(event, payload, { category })` sends or queues
|
|
459
|
+
an event, `Consently.granted(category)` says where things stand, and a
|
|
460
|
+
`consently:change` event fires on `document` with the granted categories.
|
|
409
461
|
|
|
410
462
|
## Licence
|
|
411
463
|
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
--consently-radius: 0.75rem;
|
|
24
24
|
--consently-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
25
25
|
--consently-max-width: 36rem;
|
|
26
|
+
--consently-checkbox-border: #d1d5db;
|
|
27
|
+
--consently-checkbox-radius: 0.25rem;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
.consently {
|
|
@@ -90,11 +92,46 @@
|
|
|
90
92
|
line-height: 1.5rem;
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
/* Drawn by hand rather than left native: CSS resets in the host app
|
|
96
|
+
(@tailwindcss/forms and friends) restyle every [type=checkbox] on the page
|
|
97
|
+
and the banner ends up with square boxes in the wrong colour. These
|
|
98
|
+
selectors outrank such resets, so the box looks the same everywhere. */
|
|
99
|
+
.consently__option input[type="checkbox"] {
|
|
100
|
+
-webkit-appearance: none;
|
|
101
|
+
appearance: none;
|
|
102
|
+
flex: none;
|
|
103
|
+
box-sizing: border-box;
|
|
104
|
+
display: inline-block;
|
|
105
|
+
margin: 0.25rem 0 0;
|
|
106
|
+
padding: 0;
|
|
95
107
|
width: 1rem;
|
|
96
108
|
height: 1rem;
|
|
97
|
-
|
|
109
|
+
border: 1px solid var(--consently-checkbox-border);
|
|
110
|
+
border-radius: var(--consently-checkbox-radius);
|
|
111
|
+
background: var(--consently-background) none center / 100% 100% no-repeat;
|
|
112
|
+
box-shadow: none;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.consently__option input[type="checkbox"]:checked {
|
|
117
|
+
border-color: transparent;
|
|
118
|
+
background-color: var(--consently-accent);
|
|
119
|
+
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.consently__option input[type="checkbox"]:disabled {
|
|
123
|
+
cursor: default;
|
|
124
|
+
opacity: 0.6;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.consently__option input[type="checkbox"]:focus {
|
|
128
|
+
box-shadow: none;
|
|
129
|
+
outline: none;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.consently__option input[type="checkbox"]:focus-visible {
|
|
133
|
+
outline: 2px solid var(--consently-accent);
|
|
134
|
+
outline-offset: 2px;
|
|
98
135
|
}
|
|
99
136
|
|
|
100
137
|
.consently__option-name {
|
|
@@ -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|
|
|
@@ -42,6 +42,8 @@ module Consently
|
|
|
42
42
|
# `consently_preferences_link` has something to reopen.
|
|
43
43
|
def consently_banner(policy_url: nil)
|
|
44
44
|
return "".html_safe unless consently_enabled? && Consently.consent_required?(request)
|
|
45
|
+
# Only necessary cookies: nothing to ask, so no banner.
|
|
46
|
+
return "".html_safe if Consently.config.optional_categories.empty?
|
|
45
47
|
|
|
46
48
|
render "consently/banner",
|
|
47
49
|
consent: consently_consent,
|
|
@@ -75,17 +77,21 @@ module Consently
|
|
|
75
77
|
end
|
|
76
78
|
end
|
|
77
79
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
+
# An analytics event from a view, sent the way this page's tags expect -
|
|
81
|
+
# gtag('event', ...) under gtag.js, a dataLayer push under Google Tag
|
|
82
|
+
# Manager (see Consently.event_transport_for) - and only with consent.
|
|
83
|
+
# Without it the event is held back the way a tag is: an inert script
|
|
84
|
+
# the banner releases the moment the category is granted, so the page a
|
|
85
|
+
# visitor accepts on still counts.
|
|
80
86
|
#
|
|
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});"
|
|
87
|
+
# <%= consently_event("newsletter_signup", source: "footer") %>
|
|
88
|
+
def consently_event(event, category: :analytics, **payload)
|
|
89
|
+
consently_event_tag(Event.new(event, payload), category)
|
|
87
90
|
end
|
|
88
91
|
|
|
92
|
+
# The name this helper had before it learnt to speak gtag.
|
|
93
|
+
alias_method :consently_data_layer_push, :consently_event
|
|
94
|
+
|
|
89
95
|
# A GA4 ecommerce event in the shape Google expects, from whatever your
|
|
90
96
|
# models happen to look like:
|
|
91
97
|
#
|
|
@@ -96,19 +102,32 @@ module Consently
|
|
|
96
102
|
# few obvious names (id/sku, name, price, quantity, category, brand,
|
|
97
103
|
# variant) - a LineItem or a Product usually does.
|
|
98
104
|
#
|
|
99
|
-
#
|
|
100
|
-
# events on one page cannot bleed into
|
|
105
|
+
# Under Google Tag Manager the previous ecommerce object is cleared
|
|
106
|
+
# first, as Google asks, so two events on one page cannot bleed into
|
|
107
|
+
# each other. Held back without consent, like consently_event.
|
|
101
108
|
def consently_ecommerce(event, items: [], category: :analytics, **params)
|
|
102
|
-
|
|
109
|
+
consently_event_tag(Event.new(event, params.merge(items: consently_ecommerce_items(items)), ecommerce: true), category)
|
|
110
|
+
end
|
|
103
111
|
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
# The same event from a Turbo Stream response - an add-to-cart that
|
|
113
|
+
# never renders a page. Items make it an ecommerce event. The stream
|
|
114
|
+
# carries the event, not a script: the JavaScript side sends it at once
|
|
115
|
+
# if the category is granted and keeps it until then otherwise, so a
|
|
116
|
+
# cart filled before the click is counted after it.
|
|
117
|
+
#
|
|
118
|
+
# <%= consently_stream_event("add_to_cart", items: [ @line_item ], currency: "EUR", value: 12.5) %>
|
|
119
|
+
def consently_stream_event(event, items: nil, category: :analytics, **payload)
|
|
120
|
+
return "".html_safe unless consently_enabled?
|
|
106
121
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
122
|
+
payload = payload.merge(items: consently_ecommerce_items(items)) unless items.nil?
|
|
123
|
+
event = Event.new(event, payload, ecommerce: !items.nil?)
|
|
124
|
+
|
|
125
|
+
content_tag("turbo-stream", "",
|
|
126
|
+
action: "consently_event",
|
|
127
|
+
event: event.name,
|
|
128
|
+
category: category,
|
|
129
|
+
ecommerce: event.ecommerce?,
|
|
130
|
+
payload: event.payload.to_json)
|
|
112
131
|
end
|
|
113
132
|
|
|
114
133
|
# A complete cookie policy for the tags this request would load: every
|
|
@@ -132,6 +151,13 @@ module Consently
|
|
|
132
151
|
# button that opens the preferences panel; the iframe appears the moment
|
|
133
152
|
# they agree, without a reload.
|
|
134
153
|
def consently_embed(kind, identifier, category: :marketing, title: nil, ratio: "16 / 9", **iframe_options)
|
|
154
|
+
# The placeholder sends the visitor to a checkbox; without one the
|
|
155
|
+
# embed could never be released.
|
|
156
|
+
unless Consently.config.category?(category)
|
|
157
|
+
raise ArgumentError, "consently_embed waits for the #{category} category, which the banner does not offer - " \
|
|
158
|
+
"add `c.category :#{category}` to config/initializers/consently.rb"
|
|
159
|
+
end
|
|
160
|
+
|
|
135
161
|
src = consently_embed_src(kind, identifier)
|
|
136
162
|
return "".html_safe if src.blank?
|
|
137
163
|
|
|
@@ -167,6 +193,40 @@ module Consently
|
|
|
167
193
|
Consently.enabled?(request)
|
|
168
194
|
end
|
|
169
195
|
|
|
196
|
+
# Two <meta> tags the JavaScript side reads: which categories this page
|
|
197
|
+
# was rendered with, and how events are to be sent. Provisional head
|
|
198
|
+
# elements, so Turbo Drive swaps them on every visit.
|
|
199
|
+
def consently_state_tags
|
|
200
|
+
granted = Consently.config.categories.select { |category| consently_consent.granted?(category) }
|
|
201
|
+
|
|
202
|
+
safe_join([
|
|
203
|
+
tag.meta(name: "consently-granted", content: granted.join(" ")),
|
|
204
|
+
tag.meta(name: "consently-transport", content: consently_event_transport)
|
|
205
|
+
], "\n")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def consently_event_transport
|
|
209
|
+
@consently_event_transport ||= Consently.event_transport_for(request)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Live when the category is granted; otherwise inert, and released by the
|
|
213
|
+
# banner together with the tags the moment it is.
|
|
214
|
+
def consently_event_tag(event, category)
|
|
215
|
+
return "".html_safe unless consently_enabled?
|
|
216
|
+
|
|
217
|
+
javascript = event.to_js(consently_event_transport)
|
|
218
|
+
return consently_inline_script(javascript) if consently_consent.granted?(category)
|
|
219
|
+
|
|
220
|
+
attributes = { type: "text/plain", data: { "consently-category" => category } }
|
|
221
|
+
attributes[:nonce] = content_security_policy_nonce if content_security_policy_nonce.present?
|
|
222
|
+
|
|
223
|
+
content_tag(:script, javascript.html_safe, attributes)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def consently_ecommerce_items(items)
|
|
227
|
+
Array(items).map { |item| consently_ecommerce_item(item) }
|
|
228
|
+
end
|
|
229
|
+
|
|
170
230
|
# Whether this tag may run now. Normally that means consent; under
|
|
171
231
|
# advanced consent mode Google's own tags also load before it, having
|
|
172
232
|
# 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 })
|
|
@@ -32,7 +32,9 @@
|
|
|
32
32
|
aria-labelledby="consently-message"
|
|
33
33
|
tabindex="-1">
|
|
34
34
|
<p class="consently__text" id="consently-message">
|
|
35
|
-
|
|
35
|
+
<%# The message names only what is asked for: a site without marketing
|
|
36
|
+
tags does not promise "relevant offers". %>
|
|
37
|
+
<%= t("consently.banner.message_#{categories.map(&:to_s).sort.join("_")}", default: t("consently.banner.message")) %>
|
|
36
38
|
<% if policy_url.present? %>
|
|
37
39
|
<%= link_to t("consently.banner.policy_link"), policy_url, class: "consently__link" %>
|
|
38
40
|
<% end %>
|
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
|
data/config/locales/cs.yml
CHANGED
|
@@ -2,6 +2,8 @@ cs:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Používáme soubory cookie, aby tento web fungoval, a s vaším souhlasem také k tomu, abychom rozuměli, jak jej používáte, a mohli vám ukazovat relevantní nabídky."
|
|
5
|
+
message_analytics: "Používáme soubory cookie, aby tento web fungoval, a s vaším souhlasem také k tomu, abychom rozuměli, jak jej používáte, a mohli jej zlepšovat."
|
|
6
|
+
message_marketing: "Používáme soubory cookie, aby tento web fungoval, a s vaším souhlasem také k tomu, abychom vám mohli ukazovat relevantní nabídky."
|
|
5
7
|
policy_link: "Přečtěte si naše zásady cookies."
|
|
6
8
|
accept_all: "Přijmout vše"
|
|
7
9
|
reject_all: "Odmítnout vše"
|
data/config/locales/de.yml
CHANGED
|
@@ -2,6 +2,8 @@ de:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Wir verwenden Cookies, damit diese Website funktioniert - und mit Ihrer Einwilligung, um zu verstehen, wie sie genutzt wird, und Ihnen passende Angebote zu zeigen."
|
|
5
|
+
message_analytics: "Wir verwenden Cookies, damit diese Website funktioniert - und mit Ihrer Einwilligung, um zu verstehen, wie sie genutzt wird, und sie zu verbessern."
|
|
6
|
+
message_marketing: "Wir verwenden Cookies, damit diese Website funktioniert - und mit Ihrer Einwilligung, um Ihnen passende Angebote zu zeigen."
|
|
5
7
|
policy_link: "Zur Cookie-Richtlinie."
|
|
6
8
|
accept_all: "Alle akzeptieren"
|
|
7
9
|
reject_all: "Alle ablehnen"
|
data/config/locales/en.yml
CHANGED
|
@@ -2,6 +2,8 @@ en:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "We use cookies to run this site, and - with your consent - to understand how it is used and to show you relevant offers."
|
|
5
|
+
message_analytics: "We use cookies to run this site, and - with your consent - to understand how it is used and improve it."
|
|
6
|
+
message_marketing: "We use cookies to run this site, and - with your consent - to show you relevant offers."
|
|
5
7
|
policy_link: "Read our cookie policy."
|
|
6
8
|
accept_all: "Accept all"
|
|
7
9
|
reject_all: "Reject all"
|
data/config/locales/es.yml
CHANGED
|
@@ -2,6 +2,8 @@ es:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Usamos cookies para que este sitio funcione y, con tu consentimiento, para entender cómo se usa y mostrarte ofertas relevantes."
|
|
5
|
+
message_analytics: "Usamos cookies para que este sitio funcione y, con tu consentimiento, para entender cómo se usa y mejorarlo."
|
|
6
|
+
message_marketing: "Usamos cookies para que este sitio funcione y, con tu consentimiento, para mostrarte ofertas relevantes."
|
|
5
7
|
policy_link: "Lee nuestra política de cookies."
|
|
6
8
|
accept_all: "Aceptar todo"
|
|
7
9
|
reject_all: "Rechazar todo"
|
data/config/locales/fr.yml
CHANGED
|
@@ -2,6 +2,8 @@ fr:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Nous utilisons des cookies pour faire fonctionner ce site et, avec votre accord, pour comprendre comment il est utilisé et vous proposer des offres pertinentes."
|
|
5
|
+
message_analytics: "Nous utilisons des cookies pour faire fonctionner ce site et, avec votre accord, pour comprendre comment il est utilisé et l'améliorer."
|
|
6
|
+
message_marketing: "Nous utilisons des cookies pour faire fonctionner ce site et, avec votre accord, pour vous proposer des offres pertinentes."
|
|
5
7
|
policy_link: "Consulter notre politique de cookies."
|
|
6
8
|
accept_all: "Tout accepter"
|
|
7
9
|
reject_all: "Tout refuser"
|
data/config/locales/hu.yml
CHANGED
|
@@ -2,6 +2,8 @@ hu:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Sütiket használunk, hogy az oldal működjön, és - a hozzájárulásoddal - hogy megértsük, hogyan használod, illetve releváns ajánlatokat mutassunk."
|
|
5
|
+
message_analytics: "Sütiket használunk, hogy az oldal működjön, és - a hozzájárulásoddal - hogy megértsük, hogyan használod, és fejleszthessük."
|
|
6
|
+
message_marketing: "Sütiket használunk, hogy az oldal működjön, és - a hozzájárulásoddal - hogy releváns ajánlatokat mutassunk."
|
|
5
7
|
policy_link: "Olvasd el a süti szabályzatunkat."
|
|
6
8
|
accept_all: "Összes elfogadása"
|
|
7
9
|
reject_all: "Összes elutasítása"
|
data/config/locales/it.yml
CHANGED
|
@@ -2,6 +2,8 @@ it:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Usiamo i cookie per far funzionare questo sito e, con il tuo consenso, per capire come viene usato e mostrarti offerte pertinenti."
|
|
5
|
+
message_analytics: "Usiamo i cookie per far funzionare questo sito e, con il tuo consenso, per capire come viene usato e migliorarlo."
|
|
6
|
+
message_marketing: "Usiamo i cookie per far funzionare questo sito e, con il tuo consenso, per mostrarti offerte pertinenti."
|
|
5
7
|
policy_link: "Leggi la nostra cookie policy."
|
|
6
8
|
accept_all: "Accetta tutto"
|
|
7
9
|
reject_all: "Rifiuta tutto"
|
data/config/locales/nl.yml
CHANGED
|
@@ -2,6 +2,8 @@ nl:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "We gebruiken cookies om deze site te laten werken en, met jouw toestemming, om te begrijpen hoe de site gebruikt wordt en je relevante aanbiedingen te tonen."
|
|
5
|
+
message_analytics: "We gebruiken cookies om deze site te laten werken en, met jouw toestemming, om te begrijpen hoe de site gebruikt wordt en hem te verbeteren."
|
|
6
|
+
message_marketing: "We gebruiken cookies om deze site te laten werken en, met jouw toestemming, om je relevante aanbiedingen te tonen."
|
|
5
7
|
policy_link: "Lees ons cookiebeleid."
|
|
6
8
|
accept_all: "Alles accepteren"
|
|
7
9
|
reject_all: "Alles weigeren"
|
data/config/locales/pl.yml
CHANGED
|
@@ -2,6 +2,8 @@ pl:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Używamy plików cookie, żeby ta strona działała, a za Twoją zgodą także po to, by rozumieć, jak z niej korzystasz, i pokazywać Ci trafne oferty."
|
|
5
|
+
message_analytics: "Używamy plików cookie, żeby ta strona działała, a za Twoją zgodą także po to, by rozumieć, jak z niej korzystasz, i ją ulepszać."
|
|
6
|
+
message_marketing: "Używamy plików cookie, żeby ta strona działała, a za Twoją zgodą także po to, by pokazywać Ci trafne oferty."
|
|
5
7
|
policy_link: "Przeczytaj naszą politykę cookies."
|
|
6
8
|
accept_all: "Akceptuję wszystkie"
|
|
7
9
|
reject_all: "Odrzuć wszystkie"
|
data/config/locales/sk.yml
CHANGED
|
@@ -2,6 +2,8 @@ sk:
|
|
|
2
2
|
consently:
|
|
3
3
|
banner:
|
|
4
4
|
message: "Používame súbory cookie, aby táto stránka fungovala, a s vaším súhlasom aj na to, aby sme rozumeli, ako ju používate, a mohli vám ukazovať relevantné ponuky."
|
|
5
|
+
message_analytics: "Používame súbory cookie, aby táto stránka fungovala, a s vaším súhlasom aj na to, aby sme rozumeli, ako ju používate, a mohli ju zlepšovať."
|
|
6
|
+
message_marketing: "Používame súbory cookie, aby táto stránka fungovala, a s vaším súhlasom aj na to, aby sme vám mohli ukazovať relevantné ponuky."
|
|
5
7
|
policy_link: "Prečítajte si naše zásady cookies."
|
|
6
8
|
accept_all: "Prijať všetko"
|
|
7
9
|
reject_all: "Odmietnuť všetko"
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module Consently
|
|
2
2
|
# Everything the host application sets in config/initializers/consently.rb.
|
|
3
3
|
class Configuration
|
|
4
|
-
|
|
4
|
+
# The categories Consently knows by name, in the order the banner lists
|
|
5
|
+
# them. Only the ones in use are offered - see #categories.
|
|
6
|
+
BUILT_IN_CATEGORIES = %i[necessary analytics marketing].freeze
|
|
5
7
|
|
|
6
8
|
# Where the visitor's choice is kept. It is read by JavaScript, so it is a
|
|
7
9
|
# plain cookie rather than a signed one.
|
|
@@ -50,6 +52,21 @@ module Consently
|
|
|
50
52
|
google_consent_mode == :advanced
|
|
51
53
|
end
|
|
52
54
|
|
|
55
|
+
# How events are sent: :gtag for a page running gtag.js, :data_layer for
|
|
56
|
+
# one running Google Tag Manager. The default, :auto, decides per request
|
|
57
|
+
# from the tags in its scope (see Consently.event_transport_for) and is
|
|
58
|
+
# right unless you push to a container the gem does not know about.
|
|
59
|
+
attr_reader :event_transport
|
|
60
|
+
|
|
61
|
+
def event_transport=(transport)
|
|
62
|
+
transport = transport&.to_sym
|
|
63
|
+
unless [ :auto, *Event::TRANSPORTS ].include?(transport)
|
|
64
|
+
raise ArgumentError, "event_transport must be :auto, :gtag or :data_layer"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@event_transport = transport
|
|
68
|
+
end
|
|
69
|
+
|
|
53
70
|
# Store a row per decision, as proof of consent. Needs the engine mounted
|
|
54
71
|
# and the migration from `rails g consently:consent_log`.
|
|
55
72
|
attr_accessor :log_consents
|
|
@@ -98,8 +115,6 @@ module Consently
|
|
|
98
115
|
# Nil means every request sees the default configuration.
|
|
99
116
|
attr_accessor :scope_resolver
|
|
100
117
|
|
|
101
|
-
attr_reader :categories
|
|
102
|
-
|
|
103
118
|
def initialize
|
|
104
119
|
@cookie_name = "consently"
|
|
105
120
|
@cookie_max_age = 60 * 60 * 24 * 180 # six months, the usual guidance
|
|
@@ -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
|
|
@@ -118,18 +134,45 @@ module Consently
|
|
|
118
134
|
@consent_subject = nil
|
|
119
135
|
@policy_url = nil
|
|
120
136
|
@scope_resolver = nil
|
|
121
|
-
@categories =
|
|
137
|
+
@categories = nil
|
|
138
|
+
@declared_categories = []
|
|
122
139
|
@scopes = {}
|
|
123
140
|
end
|
|
124
141
|
|
|
125
|
-
#
|
|
126
|
-
#
|
|
142
|
+
# The categories a visitor is asked about, `necessary` always first.
|
|
143
|
+
#
|
|
144
|
+
# Worked out from the tags (in every scope) plus whatever was declared with
|
|
145
|
+
# `category`: a site running only Google Analytics asks about analytics and
|
|
146
|
+
# nothing else. Asking for consent to marketing nobody does is not just
|
|
147
|
+
# noise - it tells the visitor something about the site that is not true.
|
|
148
|
+
#
|
|
149
|
+
# `categories=` replaces the guess with a fixed list, for categories that
|
|
150
|
+
# only views use (an embed, an event) or to keep the order your own.
|
|
151
|
+
def categories
|
|
152
|
+
return @categories if @categories
|
|
153
|
+
|
|
154
|
+
in_use = (all_tags.map(&:category) + @declared_categories).uniq
|
|
155
|
+
[ Consent::NECESSARY, *(BUILT_IN_CATEGORIES & in_use), *(in_use - BUILT_IN_CATEGORIES) ].uniq
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def categories=(names)
|
|
159
|
+
@categories = [ Consent::NECESSARY, *Array(names).map(&:to_sym) ].uniq
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A category to offer even with no tag in it - one of your own, or a
|
|
163
|
+
# built-in one that only an embed or an event uses. Give your own a name
|
|
164
|
+
# and a description in your locale file.
|
|
127
165
|
def category(name)
|
|
128
166
|
name = name.to_sym
|
|
129
|
-
@
|
|
167
|
+
@declared_categories << name unless @declared_categories.include?(name)
|
|
168
|
+
@categories << name if @categories && !@categories.include?(name)
|
|
130
169
|
name
|
|
131
170
|
end
|
|
132
171
|
|
|
172
|
+
def category?(name)
|
|
173
|
+
categories.include?(name.to_sym)
|
|
174
|
+
end
|
|
175
|
+
|
|
133
176
|
def optional_categories
|
|
134
177
|
categories - [ Consent::NECESSARY ]
|
|
135
178
|
end
|
|
@@ -162,6 +205,12 @@ module Consently
|
|
|
162
205
|
@default_scope ||= Scope.new
|
|
163
206
|
end
|
|
164
207
|
|
|
208
|
+
# Every tag in every scope, overridden ones included: a category one
|
|
209
|
+
# tenant needs is offered to all, since the banner is the same for each.
|
|
210
|
+
def all_tags
|
|
211
|
+
[ default_scope, *@scopes.values ].flat_map { |scope| scope.tags.values }
|
|
212
|
+
end
|
|
213
|
+
|
|
165
214
|
# A named bundle of tags. Same API as the top-level `tag`, which is what
|
|
166
215
|
# makes the block form read the way it does.
|
|
167
216
|
class Scope
|
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)
|
|
@@ -44,7 +44,9 @@ c.google_consent_mode = :basic
|
|
|
44
44
|
# The log is anonymous unless you say who made the decision:
|
|
45
45
|
# c.consent_subject = ->(request) { request.env["warden"]&.user&.id }
|
|
46
46
|
|
|
47
|
-
#
|
|
47
|
+
# The banner offers only the categories your tags use. Declare one that only
|
|
48
|
+
# an embed or an event needs, or one of your own:
|
|
49
|
+
# c.category :marketing
|
|
48
50
|
# c.category :personalization
|
|
49
51
|
|
|
50
52
|
# Your tags. Each one waits for its category unless that category is
|
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: 2.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.20
|
|
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
|