consently 0.1.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 +7 -0
- data/CHANGELOG.md +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +279 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/consently.css +252 -0
- data/app/controllers/consently/application_controller.rb +4 -0
- data/app/controllers/consently/consents_controller.rb +44 -0
- data/app/helpers/consently/application_helper.rb +4 -0
- data/app/helpers/consently/tags_helper.rb +176 -0
- data/app/javascript/consently/banner_controller.js +190 -0
- data/app/models/consently/application_record.rb +5 -0
- data/app/models/consently/consent_record.rb +18 -0
- data/app/views/consently/_banner.html.erb +93 -0
- data/app/views/consently/_policy.html.erb +85 -0
- data/config/importmap.rb +3 -0
- data/config/locales/cs.yml +33 -0
- data/config/locales/de.yml +33 -0
- data/config/locales/en.yml +33 -0
- data/config/locales/es.yml +33 -0
- data/config/locales/fr.yml +33 -0
- data/config/locales/hu.yml +33 -0
- data/config/locales/it.yml +33 -0
- data/config/locales/nl.yml +33 -0
- data/config/locales/pl.yml +33 -0
- data/config/locales/sk.yml +33 -0
- data/config/routes.rb +5 -0
- data/lib/consently/configuration.rb +150 -0
- data/lib/consently/consent.rb +62 -0
- data/lib/consently/cookie.rb +12 -0
- data/lib/consently/engine.rb +33 -0
- data/lib/consently/providers/base.rb +95 -0
- data/lib/consently/providers/clarity.rb +34 -0
- data/lib/consently/providers/custom.rb +34 -0
- data/lib/consently/providers/google_ads.rb +32 -0
- data/lib/consently/providers/google_analytics.rb +45 -0
- data/lib/consently/providers/google_tag_manager.rb +34 -0
- data/lib/consently/providers/hotjar.rb +39 -0
- data/lib/consently/providers/meta_pixel.rb +37 -0
- data/lib/consently/providers/plausible.rb +30 -0
- data/lib/consently/providers.rb +9 -0
- data/lib/consently/script.rb +17 -0
- data/lib/consently/version.rb +3 -0
- data/lib/consently.rb +74 -0
- data/lib/generators/consently/consent_log/consent_log_generator.rb +28 -0
- data/lib/generators/consently/consent_log/templates/create_consently_consent_records.rb +19 -0
- data/lib/generators/consently/install/install_generator.rb +44 -0
- data/lib/generators/consently/install/templates/consently.rb +63 -0
- data/lib/generators/consently/views/views_generator.rb +15 -0
- data/lib/tasks/consently_tasks.rake +4 -0
- metadata +121 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
# The escape hatch for a vendor Consently does not know about yet. Blocked
|
|
4
|
+
# and released exactly like a built-in one.
|
|
5
|
+
#
|
|
6
|
+
# c.tag :custom, as: :piwik, category: :analytics,
|
|
7
|
+
# src: "https://cdn.example.com/piwik.js"
|
|
8
|
+
#
|
|
9
|
+
# c.tag :custom, as: :whatever, category: :marketing, inline: <<~JS
|
|
10
|
+
# console.log("hello");
|
|
11
|
+
# JS
|
|
12
|
+
class Custom < Base
|
|
13
|
+
self.provider_key = :custom
|
|
14
|
+
self.default_category = :marketing
|
|
15
|
+
|
|
16
|
+
def scripts
|
|
17
|
+
[ Script.new(src: options[:src], inline: options[:inline], async: options.fetch(:async, true), data: options[:data]) ]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def noscript
|
|
21
|
+
options[:noscript]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def validate!
|
|
27
|
+
if options[:src].blank? && options[:inline].blank?
|
|
28
|
+
raise MissingOption, "Consently: a :custom tag needs src: or inline:"
|
|
29
|
+
end
|
|
30
|
+
raise MissingOption, "Consently: a :custom tag needs as: to name it" if options[:as].blank?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
class GoogleAds < Base
|
|
4
|
+
self.provider_key = :google_ads
|
|
5
|
+
self.default_category = :marketing
|
|
6
|
+
|
|
7
|
+
cookie "_gcl_au", days: 90
|
|
8
|
+
|
|
9
|
+
def scripts
|
|
10
|
+
[
|
|
11
|
+
Script.new(src: "https://www.googletagmanager.com/gtag/js?id=#{id}", async: true),
|
|
12
|
+
Script.new(inline: <<~JS.strip)
|
|
13
|
+
window.dataLayer = window.dataLayer || [];
|
|
14
|
+
function gtag(){dataLayer.push(arguments);}
|
|
15
|
+
gtag('js', new Date());
|
|
16
|
+
gtag('config', '#{id}');
|
|
17
|
+
JS
|
|
18
|
+
]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def id
|
|
24
|
+
@id ||= require_option(:id)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate!
|
|
28
|
+
id
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
# GA4. Pass `anonymize: false` to leave IP anonymisation off; it is on by
|
|
4
|
+
# default because most European deployments want it.
|
|
5
|
+
class GoogleAnalytics < Base
|
|
6
|
+
self.provider_key = :google_analytics
|
|
7
|
+
self.default_category = :analytics
|
|
8
|
+
|
|
9
|
+
# What GA4 leaves behind. The container-scoped one is written per
|
|
10
|
+
# measurement id, hence the wildcard.
|
|
11
|
+
cookie "_ga", days: 730
|
|
12
|
+
cookie "_ga_*", days: 730
|
|
13
|
+
cookie "_gid", days: 1
|
|
14
|
+
cookie "_gat", days: nil
|
|
15
|
+
|
|
16
|
+
def scripts
|
|
17
|
+
[
|
|
18
|
+
Script.new(src: "https://www.googletagmanager.com/gtag/js?id=#{id}", async: true),
|
|
19
|
+
Script.new(inline: <<~JS.strip)
|
|
20
|
+
window.dataLayer = window.dataLayer || [];
|
|
21
|
+
function gtag(){dataLayer.push(arguments);}
|
|
22
|
+
gtag('js', new Date());
|
|
23
|
+
gtag('config', '#{id}'#{config_options});
|
|
24
|
+
JS
|
|
25
|
+
]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def id
|
|
31
|
+
@id ||= require_option(:id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def config_options
|
|
35
|
+
return "" unless options.fetch(:anonymize, true)
|
|
36
|
+
|
|
37
|
+
", { 'anonymize_ip': true }"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate!
|
|
41
|
+
id
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
class GoogleTagManager < Base
|
|
4
|
+
self.provider_key = :google_tag_manager
|
|
5
|
+
self.default_category = :analytics
|
|
6
|
+
|
|
7
|
+
# None of its own: whatever it loads brings its own cookies, so list
|
|
8
|
+
# those tags here as well if you manage them through the container.
|
|
9
|
+
|
|
10
|
+
def scripts
|
|
11
|
+
[ Script.new(inline: <<~JS.strip) ]
|
|
12
|
+
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
|
|
13
|
+
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
|
|
14
|
+
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
15
|
+
})(window,document,'script','dataLayer','#{id}');
|
|
16
|
+
JS
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def noscript
|
|
20
|
+
%(<iframe src="https://www.googletagmanager.com/ns.html?id=#{id}" height="0" width="0" style="display:none;visibility:hidden"></iframe>)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def id
|
|
26
|
+
@id ||= require_option(:id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate!
|
|
30
|
+
id
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
class Hotjar < Base
|
|
4
|
+
self.provider_key = :hotjar
|
|
5
|
+
self.default_category = :analytics
|
|
6
|
+
|
|
7
|
+
cookie "_hjSessionUser_*", days: 365
|
|
8
|
+
cookie "_hjSession_*", days: nil
|
|
9
|
+
|
|
10
|
+
def scripts
|
|
11
|
+
[ Script.new(inline: <<~JS.strip) ]
|
|
12
|
+
(function(h,o,t,j,a,r){h.hj=h.hj||function(){(h.hj.q=h.hj.q||[]).push(arguments)};
|
|
13
|
+
h._hjSettings={hjid:#{id},hjsv:#{version}};a=o.getElementsByTagName('head')[0];
|
|
14
|
+
r=o.createElement('script');r.async=1;r.src=t+h._hjSettings.hjid+j+h._hjSettings.hjsv;
|
|
15
|
+
a.appendChild(r);})(window,document,'https://static.hotjar.com/c/hotjar-','.js?sv=');
|
|
16
|
+
JS
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Numeric on purpose: the snippet uses the id unquoted.
|
|
22
|
+
def id
|
|
23
|
+
@id ||= Integer(require_option(:id))
|
|
24
|
+
rescue ArgumentError
|
|
25
|
+
raise MissingOption, "Consently: hotjar id: must be a number"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Hotjar's snippet version, not the site id; it has been 6 for years.
|
|
29
|
+
def version
|
|
30
|
+
Integer(options.fetch(:snippet_version, 6))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def validate!
|
|
34
|
+
id
|
|
35
|
+
version
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
class MetaPixel < Base
|
|
4
|
+
self.provider_key = :meta_pixel
|
|
5
|
+
self.default_category = :marketing
|
|
6
|
+
|
|
7
|
+
cookie "_fbp", days: 90
|
|
8
|
+
cookie "fr", days: 90
|
|
9
|
+
|
|
10
|
+
def scripts
|
|
11
|
+
[ Script.new(inline: <<~JS.strip) ]
|
|
12
|
+
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
|
13
|
+
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
|
14
|
+
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
|
|
15
|
+
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
|
|
16
|
+
document,'script','https://connect.facebook.net/en_US/fbevents.js');
|
|
17
|
+
fbq('init', '#{id}');
|
|
18
|
+
fbq('track', 'PageView');
|
|
19
|
+
JS
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def noscript
|
|
23
|
+
%(<img height="1" width="1" style="display:none" alt="" src="https://www.facebook.com/tr?id=#{id}&ev=PageView&noscript=1">)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def id
|
|
29
|
+
@id ||= require_option(:id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def validate!
|
|
33
|
+
id
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
# Cookieless, and often run without a banner at all - but whether that is
|
|
4
|
+
# allowed is a legal call, not ours, so it waits for consent like any
|
|
5
|
+
# other analytics tag. Move it with category: :necessary if your legal
|
|
6
|
+
# advice says it may run right away.
|
|
7
|
+
class Plausible < Base
|
|
8
|
+
self.provider_key = :plausible
|
|
9
|
+
self.default_category = :analytics
|
|
10
|
+
|
|
11
|
+
def scripts
|
|
12
|
+
[ Script.new(src: "#{host}/js/script.js", defer: true, data: { "domain" => domain }) ]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def domain
|
|
18
|
+
@domain ||= require_option(:domain)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def host
|
|
22
|
+
@host ||= options.fetch(:host, "https://plausible.io").to_s.chomp("/")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate!
|
|
26
|
+
domain
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
require "consently/providers/base"
|
|
2
|
+
require "consently/providers/google_tag_manager"
|
|
3
|
+
require "consently/providers/google_analytics"
|
|
4
|
+
require "consently/providers/google_ads"
|
|
5
|
+
require "consently/providers/clarity"
|
|
6
|
+
require "consently/providers/meta_pixel"
|
|
7
|
+
require "consently/providers/hotjar"
|
|
8
|
+
require "consently/providers/plausible"
|
|
9
|
+
require "consently/providers/custom"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# One <script> a provider wants on the page - either an external file or an
|
|
3
|
+
# inline body. Keeping it as data rather than as HTML is what lets the view
|
|
4
|
+
# render the very same script either live or blocked, without the provider
|
|
5
|
+
# knowing which of the two happened.
|
|
6
|
+
Script = Struct.new(:src, :inline, :async, :defer, :data, keyword_init: true) do
|
|
7
|
+
def external?
|
|
8
|
+
src.present?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Extra data-* attributes some vendors configure the tag with, e.g.
|
|
12
|
+
# Plausible's data-domain.
|
|
13
|
+
def data_attributes
|
|
14
|
+
data || {}
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/consently.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "active_support"
|
|
3
|
+
require "active_support/core_ext/object/blank"
|
|
4
|
+
|
|
5
|
+
require "consently/version"
|
|
6
|
+
require "consently/script"
|
|
7
|
+
require "consently/cookie"
|
|
8
|
+
require "consently/consent"
|
|
9
|
+
require "consently/providers/base"
|
|
10
|
+
require "consently/providers"
|
|
11
|
+
require "consently/configuration"
|
|
12
|
+
require "consently/engine" if defined?(Rails::Engine)
|
|
13
|
+
|
|
14
|
+
# Tag snippets and cookie consent in one place.
|
|
15
|
+
#
|
|
16
|
+
# Everything the host application needs goes through `Consently.configure`;
|
|
17
|
+
# the views ask this module what to render for the current request.
|
|
18
|
+
module Consently
|
|
19
|
+
class Error < StandardError; end
|
|
20
|
+
|
|
21
|
+
# A provider key that was never registered - almost always a typo in the
|
|
22
|
+
# initializer, so the message lists what is available.
|
|
23
|
+
class UnknownProvider < Error
|
|
24
|
+
def initialize(key)
|
|
25
|
+
super("Unknown Consently provider #{key.inspect}. Available: #{Providers::Base.registry.keys.sort.join(", ")}")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class MissingOption < Error; end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configure
|
|
33
|
+
yield config
|
|
34
|
+
config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def config
|
|
38
|
+
@config ||= Configuration.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Mostly for tests and for reloading an initializer in development.
|
|
42
|
+
def reset!
|
|
43
|
+
@config = Configuration.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Tags configured for this request: the defaults, plus whatever the
|
|
47
|
+
# matching scope adds or overrides.
|
|
48
|
+
def tags_for(request = nil)
|
|
49
|
+
config.tags_for(scope_for(request))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def scope_for(request)
|
|
53
|
+
return nil unless config.scope_resolver && request
|
|
54
|
+
|
|
55
|
+
config.scope_resolver.call(request)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def enabled?(request = nil)
|
|
59
|
+
resolve(config.enabled, request)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Whether this visitor has to be asked. When they do not, every category
|
|
63
|
+
# counts as granted and no banner is rendered - see config.consent_required.
|
|
64
|
+
def consent_required?(request = nil)
|
|
65
|
+
resolve(config.consent_required, request)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def resolve(setting, request)
|
|
71
|
+
setting.respond_to?(:call) ? !!setting.call(request) : !!setting
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
require "rails/generators/active_record"
|
|
3
|
+
|
|
4
|
+
module Consently
|
|
5
|
+
module Generators
|
|
6
|
+
class ConsentLogGenerator < Rails::Generators::Base
|
|
7
|
+
include ActiveRecord::Generators::Migration
|
|
8
|
+
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
desc "Adds the table behind Consently::ConsentRecord, for proof of consent."
|
|
12
|
+
|
|
13
|
+
def create_migration_file
|
|
14
|
+
migration_template "create_consently_consent_records.rb", "db/migrate/create_consently_consent_records.rb"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def show_next_steps
|
|
18
|
+
say <<~TEXT
|
|
19
|
+
|
|
20
|
+
Next:
|
|
21
|
+
1. rails db:migrate
|
|
22
|
+
2. mount Consently::Engine => "/consently" # config/routes.rb
|
|
23
|
+
3. c.log_consents = true # config/initializers/consently.rb
|
|
24
|
+
TEXT
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class CreateConsentlyConsentRecords < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :consently_consent_records do |t|
|
|
4
|
+
t.string :categories, null: false, default: ""
|
|
5
|
+
t.string :consent_version, null: false
|
|
6
|
+
t.string :subject
|
|
7
|
+
t.string :scope
|
|
8
|
+
t.string :user_agent
|
|
9
|
+
# A salted digest rather than the address itself: enough to tell two
|
|
10
|
+
# decisions apart, not enough to identify anyone.
|
|
11
|
+
t.string :ip_hash
|
|
12
|
+
|
|
13
|
+
t.datetime :created_at, null: false
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
add_index :consently_consent_records, :created_at
|
|
17
|
+
add_index :consently_consent_records, :consent_version
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module Consently
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Creates the Consently initializer and wires the banner controller up."
|
|
9
|
+
|
|
10
|
+
def create_initializer
|
|
11
|
+
template "consently.rb", "config/initializers/consently.rb"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Stimulus is how the banner releases the blocked tags, so registering
|
|
15
|
+
# the controller is part of installing, not a step in a README nobody
|
|
16
|
+
# reads.
|
|
17
|
+
def register_stimulus_controller
|
|
18
|
+
index = "app/javascript/controllers/index.js"
|
|
19
|
+
return say_status :skip, "#{index} not found - register consently-banner yourself", :yellow unless File.exist?(index)
|
|
20
|
+
|
|
21
|
+
append_to_file index, <<~JS
|
|
22
|
+
|
|
23
|
+
import ConsentlyBannerController from "consently/banner_controller"
|
|
24
|
+
application.register("consently-banner", ConsentlyBannerController)
|
|
25
|
+
JS
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def show_readme
|
|
29
|
+
say <<~TEXT
|
|
30
|
+
|
|
31
|
+
Consently is installed. One thing left - put the helpers in your layout:
|
|
32
|
+
|
|
33
|
+
<head> <%= consently_tags %>
|
|
34
|
+
<body> <%= consently_noscript_tags %> ... <%= consently_banner %>
|
|
35
|
+
|
|
36
|
+
The banner brings its own CSS; restyle it with the custom properties
|
|
37
|
+
on .consently, or run `rails g consently:views` to take the markup over.
|
|
38
|
+
|
|
39
|
+
Consent logging (optional): rails g consently:consent_log
|
|
40
|
+
TEXT
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Consently.configure do |c|
|
|
2
|
+
# Where the tags run. Anything callable gets the request.
|
|
3
|
+
c.enabled = ->(request) { Rails.env.production? }
|
|
4
|
+
|
|
5
|
+
# Bump when your cookie policy changes: older consents stop counting and
|
|
6
|
+
# the banner asks again.
|
|
7
|
+
c.consent_version = 1
|
|
8
|
+
|
|
9
|
+
# Link shown in the banner. A callable receives the view context, which is
|
|
10
|
+
# what you want when the URL is locale dependent.
|
|
11
|
+
# c.policy_url = -> (view) { view.main_app.cookie_policy_path }
|
|
12
|
+
|
|
13
|
+
# Reload the page once a choice is made. Off by default: the tags start
|
|
14
|
+
# running in place, so there is nothing to reload for. Turn it on if the
|
|
15
|
+
# page renders differently depending on consent.
|
|
16
|
+
# c.reload_after_choice = true
|
|
17
|
+
|
|
18
|
+
# Whether this visitor has to be asked at all. Return false and nothing is
|
|
19
|
+
# held back and no banner is shown - for traffic outside the EU, if your
|
|
20
|
+
# legal advice says so.
|
|
21
|
+
# c.consent_required = ->(request) { EU.include?(request.headers["CF-IPCountry"]) }
|
|
22
|
+
|
|
23
|
+
# Global Privacy Control (binding in California and Colorado) counts as a
|
|
24
|
+
# rejection and is honoured by default. Do Not Track is advisory, so it is
|
|
25
|
+
# not - switch it on if you want it treated the same way.
|
|
26
|
+
# c.respect_do_not_track = true
|
|
27
|
+
|
|
28
|
+
# Google consent mode v2: defaults denied before any Google tag, updated
|
|
29
|
+
# when the visitor chooses. Leave on if you use anything Google.
|
|
30
|
+
c.google_consent_mode = true
|
|
31
|
+
|
|
32
|
+
# Store a row per decision as proof of consent. Needs the migration from
|
|
33
|
+
# `rails g consently:consent_log` and the engine mounted in routes.rb:
|
|
34
|
+
# mount Consently::Engine => "/consently"
|
|
35
|
+
# c.log_consents = true
|
|
36
|
+
#
|
|
37
|
+
# The log is anonymous unless you say who made the decision:
|
|
38
|
+
# c.consent_subject = ->(request) { request.env["warden"]&.user&.id }
|
|
39
|
+
|
|
40
|
+
# A category of your own, on top of :necessary, :analytics and :marketing.
|
|
41
|
+
# c.category :personalization
|
|
42
|
+
|
|
43
|
+
# Your tags. Each one waits for its category unless that category is
|
|
44
|
+
# :necessary.
|
|
45
|
+
# c.tag :google_tag_manager, id: "GTM-XXXXXXX"
|
|
46
|
+
# c.tag :google_analytics, id: "G-XXXXXXXXXX"
|
|
47
|
+
# c.tag :clarity, id: "xxxxxxxxxx"
|
|
48
|
+
# c.tag :meta_pixel, id: "000000000000000"
|
|
49
|
+
# c.tag :hotjar, id: 1234567
|
|
50
|
+
# c.tag :plausible, domain: "example.com"
|
|
51
|
+
#
|
|
52
|
+
# Anything else:
|
|
53
|
+
# c.tag :custom, as: :piwik, category: :analytics, src: "https://cdn.example.com/piwik.js"
|
|
54
|
+
|
|
55
|
+
# Different tags per domain, tenant or shop. Whatever the resolver returns
|
|
56
|
+
# is matched against the scope names below; scopes inherit the tags above
|
|
57
|
+
# and may override them by declaring the same provider again.
|
|
58
|
+
# c.scope_resolver = ->(request) { request.host }
|
|
59
|
+
#
|
|
60
|
+
# c.scope "example.com" do |s|
|
|
61
|
+
# s.tag :google_analytics, id: "G-EXAMPLE"
|
|
62
|
+
# end
|
|
63
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
require "rails/generators/base"
|
|
2
|
+
|
|
3
|
+
module Consently
|
|
4
|
+
module Generators
|
|
5
|
+
class ViewsGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("../../../../app/views/consently", __dir__)
|
|
7
|
+
|
|
8
|
+
desc "Copies the banner markup into your app so you can restyle it."
|
|
9
|
+
|
|
10
|
+
def copy_views
|
|
11
|
+
directory ".", "app/views/consently"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: consently
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Michał Krzysteczko
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
description: 'Most Rails cookie banners ask for consent and then load Google Analytics
|
|
27
|
+
anyway. Consently is the other half: declare your tags once - GA4, Google Tag Manager,
|
|
28
|
+
Google Ads, Microsoft Clarity, Meta Pixel, Hotjar, Plausible or anything custom
|
|
29
|
+
- and it renders every non-essential one as an inert script the browser will not
|
|
30
|
+
even fetch, then turns them into live scripts the instant the visitor agrees, with
|
|
31
|
+
no page reload. Google Consent Mode v2 defaults are emitted before any Google tag
|
|
32
|
+
and updated on the click. The banner and its preferences panel ship translated into
|
|
33
|
+
ten European languages, styled in plain CSS with no Tailwind and no build step,
|
|
34
|
+
and driven by one Stimulus controller. Your cookie policy page is generated from
|
|
35
|
+
the same configuration - every vendor, every cookie it sets and for how long - so
|
|
36
|
+
it cannot drift out of date. Multi-tenant applications get per-domain or per-shop
|
|
37
|
+
tag sets from a single initializer, and an optional consent log stores proof of
|
|
38
|
+
each decision in your own database, with no third-party service and nothing leaving
|
|
39
|
+
your infrastructure.'
|
|
40
|
+
email:
|
|
41
|
+
- m.krzysteczko@icloud.com
|
|
42
|
+
executables: []
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
files:
|
|
46
|
+
- CHANGELOG.md
|
|
47
|
+
- MIT-LICENSE
|
|
48
|
+
- README.md
|
|
49
|
+
- Rakefile
|
|
50
|
+
- app/assets/stylesheets/consently.css
|
|
51
|
+
- app/controllers/consently/application_controller.rb
|
|
52
|
+
- app/controllers/consently/consents_controller.rb
|
|
53
|
+
- app/helpers/consently/application_helper.rb
|
|
54
|
+
- app/helpers/consently/tags_helper.rb
|
|
55
|
+
- app/javascript/consently/banner_controller.js
|
|
56
|
+
- app/models/consently/application_record.rb
|
|
57
|
+
- app/models/consently/consent_record.rb
|
|
58
|
+
- app/views/consently/_banner.html.erb
|
|
59
|
+
- app/views/consently/_policy.html.erb
|
|
60
|
+
- config/importmap.rb
|
|
61
|
+
- config/locales/cs.yml
|
|
62
|
+
- config/locales/de.yml
|
|
63
|
+
- config/locales/en.yml
|
|
64
|
+
- config/locales/es.yml
|
|
65
|
+
- config/locales/fr.yml
|
|
66
|
+
- config/locales/hu.yml
|
|
67
|
+
- config/locales/it.yml
|
|
68
|
+
- config/locales/nl.yml
|
|
69
|
+
- config/locales/pl.yml
|
|
70
|
+
- config/locales/sk.yml
|
|
71
|
+
- config/routes.rb
|
|
72
|
+
- lib/consently.rb
|
|
73
|
+
- lib/consently/configuration.rb
|
|
74
|
+
- lib/consently/consent.rb
|
|
75
|
+
- lib/consently/cookie.rb
|
|
76
|
+
- lib/consently/engine.rb
|
|
77
|
+
- lib/consently/providers.rb
|
|
78
|
+
- lib/consently/providers/base.rb
|
|
79
|
+
- lib/consently/providers/clarity.rb
|
|
80
|
+
- lib/consently/providers/custom.rb
|
|
81
|
+
- lib/consently/providers/google_ads.rb
|
|
82
|
+
- lib/consently/providers/google_analytics.rb
|
|
83
|
+
- lib/consently/providers/google_tag_manager.rb
|
|
84
|
+
- lib/consently/providers/hotjar.rb
|
|
85
|
+
- lib/consently/providers/meta_pixel.rb
|
|
86
|
+
- lib/consently/providers/plausible.rb
|
|
87
|
+
- lib/consently/script.rb
|
|
88
|
+
- lib/consently/version.rb
|
|
89
|
+
- lib/generators/consently/consent_log/consent_log_generator.rb
|
|
90
|
+
- lib/generators/consently/consent_log/templates/create_consently_consent_records.rb
|
|
91
|
+
- lib/generators/consently/install/install_generator.rb
|
|
92
|
+
- lib/generators/consently/install/templates/consently.rb
|
|
93
|
+
- lib/generators/consently/views/views_generator.rb
|
|
94
|
+
- lib/tasks/consently_tasks.rake
|
|
95
|
+
homepage: https://github.com/Xeross99/consently
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata:
|
|
99
|
+
homepage_uri: https://github.com/Xeross99/consently
|
|
100
|
+
bug_tracker_uri: https://github.com/Xeross99/consently/issues
|
|
101
|
+
changelog_uri: https://github.com/Xeross99/consently/blob/main/CHANGELOG.md
|
|
102
|
+
rubygems_mfa_required: 'true'
|
|
103
|
+
rdoc_options: []
|
|
104
|
+
require_paths:
|
|
105
|
+
- lib
|
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '3.2'
|
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '0'
|
|
116
|
+
requirements: []
|
|
117
|
+
rubygems_version: 4.0.3
|
|
118
|
+
specification_version: 4
|
|
119
|
+
summary: GDPR cookie consent for Rails that actually blocks your analytics tags until
|
|
120
|
+
the visitor agrees
|
|
121
|
+
test_files: []
|