diff --git a/app/controllers/api/submitter_email_clicks_controller.rb b/app/controllers/api/submitter_email_clicks_controller.rb new file mode 100644 index 00000000..a9d09a28 --- /dev/null +++ b/app/controllers/api/submitter_email_clicks_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Api + class SubmitterEmailClicksController < ApiBaseController + skip_before_action :authenticate_user! + + def create + submitter = Submitter.find_by!(slug: params[:submitter_slug]) + + if params[:t] == SubmissionEvents.build_tracking_param(submitter, 'click_email') + SubmissionEvents.create_with_tracking_data(submitter, 'click_email', request) + end + + render json: {} + end + end +end diff --git a/app/controllers/api/submitter_form_views_controller.rb b/app/controllers/api/submitter_form_views_controller.rb new file mode 100644 index 00000000..ae0d00fd --- /dev/null +++ b/app/controllers/api/submitter_form_views_controller.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Api + class SubmitterFormViewsController < ApiBaseController + skip_before_action :authenticate_user! + + def create + submitter = Submitter.find_by!(slug: params[:submitter_slug]) + + SubmissionEvents.create_with_tracking_data(submitter, 'view_form', request) + + render json: {} + end + end +end diff --git a/app/controllers/submitters_send_email_controller.rb b/app/controllers/submitters_send_email_controller.rb index 8a714537..3dfc1067 100644 --- a/app/controllers/submitters_send_email_controller.rb +++ b/app/controllers/submitters_send_email_controller.rb @@ -8,6 +8,8 @@ class SubmittersSendEmailController < ApplicationController SubmitterMailer.invitation_email(submitter).deliver_later! + SubmissionEvent.create!(submitter:, event_type: 'send_email') + submitter.sent_at ||= Time.current submitter.save! diff --git a/app/javascript/submission_form/form.vue b/app/javascript/submission_form/form.vue index 87f64b9e..19d885b9 100644 --- a/app/javascript/submission_form/form.vue +++ b/app/javascript/submission_form/form.vue @@ -493,10 +493,44 @@ export default { this.$nextTick(() => { this.recalculateButtonDisabledKey = Math.random() + + this.maybeTrackEmailClick() + this.trackViewForm() }) }, methods: { t, + maybeTrackEmailClick () { + const queryParams = new URLSearchParams(window.location.search) + + if (queryParams.has('t')) { + fetch(this.baseUrl + '/api/submitter_email_clicks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + t: queryParams.get('t'), + submitter_slug: this.submitterSlug + }) + }) + + queryParams.delete('t') + const newUrl = [window.location.pathname, queryParams.toString()].filter(Boolean).join('?') + window.history.replaceState({}, document.title, newUrl) + } + }, + trackViewForm () { + fetch(this.baseUrl + '/api/submitter_form_views', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + submitter_slug: this.submitterSlug + }) + }) + }, goToStep (step, scrollToArea = false, clickUpload = false) { this.currentStep = this.stepFields.indexOf(step) diff --git a/app/jobs/send_submitter_invitation_email_job.rb b/app/jobs/send_submitter_invitation_email_job.rb index 2bc4fddb..d53863fa 100644 --- a/app/jobs/send_submitter_invitation_email_job.rb +++ b/app/jobs/send_submitter_invitation_email_job.rb @@ -4,6 +4,8 @@ class SendSubmitterInvitationEmailJob < ApplicationJob def perform(submitter) SubmitterMailer.invitation_email(submitter).deliver_now! + SubmissionEvent.create!(submitter:, event_type: 'send_email') + submitter.sent_at ||= Time.current submitter.save end diff --git a/app/models/submission.rb b/app/models/submission.rb index 1283c7b5..830a1343 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -32,6 +32,7 @@ class Submission < ApplicationRecord belongs_to :created_by_user, class_name: 'User', optional: true has_many :submitters, dependent: :destroy + has_many :submission_events, dependent: :destroy serialize :template_fields, JSON serialize :template_schema, JSON diff --git a/app/models/submission_event.rb b/app/models/submission_event.rb new file mode 100644 index 00000000..3756b382 --- /dev/null +++ b/app/models/submission_event.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: submission_events +# +# id :bigint not null, primary key +# data :text not null +# event_timestamp :datetime not null +# event_type :string not null +# created_at :datetime not null +# updated_at :datetime not null +# submission_id :bigint not null +# submitter_id :bigint +# +# Indexes +# +# index_submission_events_on_submission_id (submission_id) +# index_submission_events_on_submitter_id (submitter_id) +# +# Foreign Keys +# +# fk_rails_... (submission_id => submissions.id) +# fk_rails_... (submitter_id => submitters.id) +# +class SubmissionEvent < ApplicationRecord + belongs_to :submission + belongs_to :submitter, optional: true + + attribute :data, :string, default: -> { {} } + attribute :event_timestamp, :datetime, default: -> { Time.current } + + serialize :data, JSON + + before_validation :set_submission_id, on: :create + + enum :event_type, { + send_email: 'send_email', + send_sms: 'send_sms', + open_email: 'open_email', + click_email: 'click_email', + click_sms: 'click_sms', + start_form: 'start_form', + view_form: 'view_form', + complete_form: 'complete_form' + }, scope: false + + private + + def set_submission_id + self.submission_id = submitter&.submission_id + end +end diff --git a/app/models/submitter.rb b/app/models/submitter.rb index e5a43573..f95eea33 100644 --- a/app/models/submitter.rb +++ b/app/models/submitter.rb @@ -44,6 +44,7 @@ class Submitter < ApplicationRecord has_many_attached :attachments has_many :document_generation_events, dependent: :destroy + has_many :submission_events, dependent: :destroy def status if completed_at? diff --git a/app/views/submitter_mailer/invitation_email.html.erb b/app/views/submitter_mailer/invitation_email.html.erb index 4f1048e0..a9ca8e26 100644 --- a/app/views/submitter_mailer/invitation_email.html.erb +++ b/app/views/submitter_mailer/invitation_email.html.erb @@ -3,7 +3,7 @@ <% else %>

Hi there,

<%= simple_format(@message) %> -

<%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug) %>

+

<%= link_to 'Submit Form', submit_form_url(slug: @submitter.slug, t: SubmissionEvents.build_tracking_param(@submitter, 'click_email')) %>

Please contact us by replying to this email if you didn't request this.

Thanks,
<%= @current_account.name %> diff --git a/config/routes.rb b/config/routes.rb index 688a5464..d49f50fe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,6 +31,8 @@ Rails.application.routes.draw do namespace :api, defaults: { format: :json } do resources :attachments, only: %i[create] + resources :submitter_email_clicks, only: %i[create] + resources :submitter_form_views, only: %i[create] resources :submissions, only: %i[create] resources :templates, only: %i[update show index] do resources :submissions, only: %i[create] diff --git a/db/migrate/20230910084410_create_submission_events.rb b/db/migrate/20230910084410_create_submission_events.rb new file mode 100644 index 00000000..4423139a --- /dev/null +++ b/db/migrate/20230910084410_create_submission_events.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class CreateSubmissionEvents < ActiveRecord::Migration[7.0] + def change + create_table :submission_events do |t| + t.references :submission, null: false, foreign_key: true, index: true + t.references :submitter, null: true, foreign_key: true, index: true + t.text :data, null: false + t.string :event_type, null: false + t.datetime :event_timestamp, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c16b0b99..0ecb94c8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_09_213212) do +ActiveRecord::Schema[7.0].define(version: 2023_09_10_084410) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -91,6 +91,18 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_09_213212) do t.index ["account_id"], name: "index_encrypted_configs_on_account_id" end + create_table "submission_events", force: :cascade do |t| + t.bigint "submission_id", null: false + t.bigint "submitter_id" + t.text "data", null: false + t.string "event_type", null: false + t.datetime "event_timestamp", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["submission_id"], name: "index_submission_events_on_submission_id" + t.index ["submitter_id"], name: "index_submission_events_on_submitter_id" + end + create_table "submissions", force: :cascade do |t| t.bigint "template_id", null: false t.bigint "created_by_user_id" @@ -178,6 +190,8 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_09_213212) do add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "document_generation_events", "submitters" add_foreign_key "encrypted_configs", "accounts" + add_foreign_key "submission_events", "submissions" + add_foreign_key "submission_events", "submitters" add_foreign_key "submissions", "templates" add_foreign_key "submissions", "users", column: "created_by_user_id" add_foreign_key "submitters", "submissions" diff --git a/lib/replace_email_variables.rb b/lib/replace_email_variables.rb index d6f88f64..0a4e6e3b 100644 --- a/lib/replace_email_variables.rb +++ b/lib/replace_email_variables.rb @@ -45,7 +45,9 @@ module ReplaceEmailVariables def build_submitter_link(submitter) Rails.application.routes.url_helpers.submit_form_url( - slug: submitter.slug, **Docuseal.default_url_options + slug: submitter.slug, + t: SubmissionEvents.build_tracking_param(submitter, 'click_email'), + **Docuseal.default_url_options ) end diff --git a/lib/submission_events.rb b/lib/submission_events.rb new file mode 100644 index 00000000..a804a5f3 --- /dev/null +++ b/lib/submission_events.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module SubmissionEvents + TRACKING_PARAM_LENGTH = 6 + + module_function + + def build_tracking_param(submitter, event_type = 'click_email') + Base64.urlsafe_encode64( + [submitter.slug, event_type, Rails.application.secrets.secret_key_base].join(':') + ).first(TRACKING_PARAM_LENGTH) + end + + def create_with_tracking_data(submitter, event_type, request) + SubmissionEvent.create!(submitter:, event_type:, data: { + ip: request.remote_ip, + ua: request.user_agent, + sid: request.session.id.to_s, + uid: request.env['warden'].user(:user)&.id + }.compact_blank) + end +end diff --git a/lib/submitters/submit_values.rb b/lib/submitters/submit_values.rb index 8385d81c..a8e79adb 100644 --- a/lib/submitters/submit_values.rb +++ b/lib/submitters/submit_values.rb @@ -7,7 +7,11 @@ module Submitters module_function def call(submitter, params, request) - Submissions.update_template_fields!(submitter.submission) if submitter.submission.template_fields.blank? + if submitter.submission.template_fields.blank? + Submissions.update_template_fields!(submitter.submission) + + SubmissionEvents.create_with_tracking_data(submitter, 'start_form', request) + end update_submitter!(submitter, params, request) @@ -32,6 +36,8 @@ module Submitters submitter.completed_at = Time.current submitter.ip = request.remote_ip submitter.ua = request.user_agent + + SubmissionEvents.create_with_tracking_data(submitter, 'complete_form', request) end submitter.save!