Giter Site home page Giter Site logo

solidusio-contrib / solidus_tracking Goto Github PK

View Code? Open in Web Editor NEW
4.0 4.0 10.0 55 KB

Data tracking extension for your Solidus store.

License: BSD 3-Clause "New" or "Revised" License

Ruby 97.20% Shell 2.80%
solidus data tracking segment ecommerce extension

solidus_tracking's Introduction

solidus_tracking

CircleCI

This extension provides a data tracking platform (think Segment, but on-premise) for your Solidus store.

It has out-of-the-box support for the most common eCommerce events, as well as for delivering transactional emails through an external service.

Service Extension
Klaviyo solidus_klaviyo

Installation

Add solidus_tracking to your Gemfile:

gem 'solidus_tracking'

Bundle your dependencies and run the installation generator:

$ bundle
$ bundle exec rails g solidus_tracking:install

The generator will create an initializer at config/initializers/solidus_tracking.rb with the default configuration. Take a look at the file and customize it to fit your environment.

Usage

Tracking events

The extension will track the following events:

  • Started Checkout: when an order transitions from the cart state to address.
  • Placed Order: when an order is finalized.
  • Ordered Product: for each item in a finalized order.
  • Fulfilled Order: when all of an order's shipments are shipped.
  • Cancelled Order: when an order is cancelled.
  • Created Account: when a user is created.
  • Reset Password: when a user requests a password reset.

For the full payload of these events, look at the source code of the serializers and events.

Implementing custom events

If you have custom events you want to track with this gem, you can easily do so by creating a new event class and implementing the required methods:

module MyApp
  module Events
    class SubscribedToNewsletter < SolidusTracking::Event::Base
      self.payload_serializer = 'MyApp::Serializers::User'

      def name
        'SubscribedToNewsletter'
      end

      def email
        user.email
      end

      def customer_properties
        self.class.customer_properties_serializer.serialize(user)
      end

      def properties
        self.class.payload_serializer.serialize(user)
      end

      def time
        Time.zone.now
      end

      private

      def user
        payload.fetch(:user)
      end
    end 
  end 
end

As you can see, you will also have to create a serializer for your users:

module MyApp
  module Serializers
    class UserSerializer < SolidusTracking::Serializer::Base
      def user
        object
      end

      def as_json(_options = {})
        {
          'FirstName' => user.first_name,
          'LastName' => user.last_name,
          # ...
        }
      end
    end
  end
end

Once you have created the event and serializer, the next step is to register your custom event when initializing the extension:

# config/initializers/solidus_tracking.rb
SolidusTracking.configure do |config|
  config.events['subscribed_to_newsletter'] = MyApp::Events::SubscribedToNewsletter
end

Your custom event is now properly configured! You can track it by calling .track_later:

SolidusTracking.track_later('subscribed_to_newsletter', user: user)

NOTE: You can follow the same exact pattern to override the built-in events.

Changing the serializers for built-in events

If you need to change the customer properties serializer or the payload serializer for one or more events, there's no need to monkey-patch or override them. You can simply set customer_properties_serializer and/or payload_serializer in an initializer:

# config/initializers/solidus_tracking.rb

SolidusTracking::Event::CancelledOrder.customer_properties_serializer = 'MyApp::Serializers::CustomerProperties'
SolidusTracking::Event::CancelledOrder.payload_serializer = 'MyApp::Serializers::Order'

If need to change the customer properties serializer for all your events (which will usually be the case), you can loop through the .events setting:

# config/initializers/solidus_tracking.rb

SolidusTracking.configuration.events.each_value do |event_klass|
  event_klass.constantize.customer_properties_serializer = 'MyApp::Serializers::CustomerProperties'
end

Make sure to do this after defining any custom events!

Delivering emails through a tracker

If you plan to deliver your transactional emails through a tracker (e.g., Klaviyo), you may want to disable the built-in emails that are delivered by Solidus and solidus_auth_devise.

In order to do that, you can set the disable_builtin_emails option in the extension's initializer:

# config/initializers/solidus_tracking.rb
SolidusTracking.configure do |config|
  config.disable_builtin_emails = true
end

This will disable the following emails:

  • Order confirmation
  • Order cancellation
  • Password reset
  • Carton shipped

You'll have to re-implement the emails with a tracker.

Disabling automatic event tracking

If you want to disable the out-of-the-box event tracking, you can do so on a per-event basis by acting on the automatic_events configuration option:

# config/initializers/solidus_tracking.rb
SolidusTracking.configure do |config|
  config.automatic_events.delete('placed_order')
end

You may also disable the built-in event tracking completely, if you only want to track events manually:

# config/initializers/solidus_tracking.rb
SolidusTracking.configure do |config|
  config.automatic_events.clear
end

Test mode

You can enable test mode to mock all API calls instead of performing them:

# config/initializers/solidus_tracking.rb
SolidusTracking.configure do |config|
  config.test_mode = true
end

This spares you the need to use VCR and similar.

When in test mode, you can also use our custom RSpec matchers to check if an event has been tracked:

require 'solidus_tracking/testing_support/matchers'

RSpec.describe 'My tracking integration' do
  it 'tracks events' do
    SolidusTracking.track_now 'custom_event', foo: 'bar'

    expect(SolidusTracking).to have_tracked_event(CustomEvent)
      .with(foo: 'bar')
  end
end

Development

Testing the extension

First bundle your dependencies, then run bin/rake. bin/rake will default to building the dummy app if it does not exist, then it will run specs. The dummy app can be regenerated by using bin/rake extension:test_app.

$ bundle
$ bin/rake

To run Rubocop static code analysis run

$ bundle exec rubocop

When testing your application's integration with this extension you may use its factories. Simply add this require statement to your spec_helper:

require 'solidus_tracking/factories'

Running the sandbox

To run this extension in a sandboxed Solidus application, you can run bin/sandbox. The path for the sandbox app is ./sandbox and bin/rails will forward any Rails commands to sandbox/bin/rails.

Here's an example:

$ bin/rails server
=> Booting Puma
=> Rails 6.0.2.1 application starting in development
* Listening on tcp://127.0.0.1:3000
Use Ctrl-C to stop

Releasing new versions

Your new extension version can be released using gem-release like this:

$ bundle exec gem bump -v VERSION --tag --push --remote upstream && gem release

License

Copyright (c) 2020 Nebulab Srls, released under the New BSD License.

solidus_tracking's People

Contributors

aldesantis avatar cpfergus1 avatar kennyadsl avatar nirebu avatar seand7565 avatar waiting-for-dev avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

solidus_tracking's Issues

Remove disable_builtin_emails

The disable_builtin_emails option was a mistake. ๐Ÿ˜… Event tracking services should be used for tracking events. Using them for sending emails is not reliable in the vast majority of cases, it's terribly complex in certain scenarios (think about resending an order receipt) and is something each app should code on its own.

Contribute event hooks back to Solidus

This extension implements some custom events on top of the Solidus event bus for hooking into the right places in the order/user lifecycle. These events should live in solidus_core, and we should contribute them upstream ASAP so that other extensions/apps can also leverage them for other purposes, and we don't have to maintain them here.

Fix race conditions in job scheduling

We are currently scheduling the tracking of certain events before the corresponding ActiveRecord instances have been saved to the DB. This causes the background jobs to fail deserialization and to be retried.

It's not a huge problem, but it unnecessarily pollutes ActiveJob queues and error monitoring systems.

Make data structures tracker-agnostic

The biggest problem with this extension's architecture, in its current state, is that the format of the Event class, as well as the names of the events and the property names in the serializers, are all specific to Klaviyo, because it was originally extracted from the source code of solidus_klaviyo.

However, the entire idea behind the extension is that it can be used with multiple trackers, not just Klaviyo.

There are two potential solutions here:

  1. Simply provide the hooks for trackers to subscribe to and fire their own events. In this scenario, each tracker provides its own event and serializer classes. While this allows maximum flexibility, it also means that there's very little reusable logic across the different trackers. If we add support for an event in solidus_tracking, each tracker will have to implement it on its own. The same goes for serializer properties. It kind of defeats the purpose of having the extension in the first place.
  2. Make the serializer and event classes more tracker-agnostic by returning more idiomatic Ruby hashes, and allow each tracker to transform the data according to the format for that tracking service. While a bit more complicated to pull off, this feels like the right path.

Event ID generation

Using numeric IDs for Klaviyo event IDs is not handy, because they can repeat themselves when DBs are reset and cause the event not to be received by Klaviyo.

Instead, we should create an ActiveRecord module that automatically generates and persists a random event ID. This can be attached to the standard events and any additional events the user wishes to send to Klaviyo.

Indicate which events are customer-originated

This extension tracks events at the lowest possible level, i.e. in model methods and state machine transitions.

This is good because it means that the event tracking won't be affected by user-facing customizations, but it may also cause some events to be tracked that are not of interest. For example, if an admin places an order from the backend, we may not want to track an "Order Completed" event, which is the current behavior.

Since we don't know whether these events should be tracked or not and we want to give users as much flexibility as possible, we should find an elegant way to clearly indicate whether an event originated from the customer or not. An example could be a global flag that indicates whether we're part of a request that was customer-initiated:

before_action :set_customer_originated

def set_customer_originated
  SolidusTracking.customer_originated = true
end

(This could also just be part of the global context, see #22.)

The user could then decide whether they want to enable/disable tracking of these events, either in a global configuration flag or on a per-event/per-plugin basis.

Allow disabling certain events

It should be possible to disable the tracking of certain events, in case the user doesn't want to track them or wants to override them with their own.

Make background job queue configurable

We should allow users to configure the name of the ActiveJob queue that will be used for scheduling event tracking jobs. This way, users can use a lower-priority queue for tracking.

config.variant_url_builder bug

Defaults as:

# A proc that accepts a variant and returns the URL of that variant's PDP.
  config.variant_url_builder = proc do |variant|
    Spree::Core::Engine.routes.url_helpers.edit_password_url(
      variant.product,
      protocol: 'https',
      host: Spree::Store.default.url,
    )
  end

note: Spree::Core::Engine.routes.url_helpers.edit_password_url

Solidus 4.0 support

I am in the process of upgrading to solidus 4.0 and this gem has a dependency error that is preventing me. Is there any known incompatibility with solidus 4.0 at the moment, or is it just the gemspec versioning that's blocking this extension from working with solidus 4.0?

Check if an order has an email before sending an event

If I have a guest user who is attempting to check out, solidus_tracking + Klaviyo will error out on the event "started_checkout" since the order does not have an email attached to it yet, since adding an email happens during checkout.

Klaviyo::KlaviyoError: You must identify a user by email or ID

Can we add a check to see if the order has an email attached before triggering the event? Or, ideally, some method that can be overritten to check for eligibility. We only submit orders to Klaviyo when a user is signed in, so we'd like to suppress all tracking for guest users, which would include all of the order lifecycle events.

order serializer fails on certain versions of solidus

https://github.com/solidusio-contrib/solidus_tracking/blob/master/lib/solidus_tracking/serializer/order.rb#L17

in Solidus 2.9.x, the line linked above fails with the following error message:

NoMethodError: undefined method 'code' for #<Spree::OrderPromotion:0x...>

the line:

'DiscountCode' => order.order_promotions.map { |op| op.code.value }.join(', '),

should be to:

'DiscountCode' => order.order_promotions.map { |op| op.promotion_code.value }.join(', '),

I'm not sure which versions of solidus use code vs promotion_code, but I'm guessing there should be something to handle pre and post refactor of the promotion codes.

Does not work if a checkout step is removed

Spent some time trying to figure out why this gem wasn't working for it. It turns out that if you remove some checkout steps the events stop firing.

      remove_checkout_step :address
      remove_checkout_step :delivery

^^ adding this will cause the events to be skipped.

Is there any suggestion on a different way I could pass over these checkout steps and keep the events occurring?

Allow specifying global context

In many cases, it's useful to send some global context along with an event, such as the request's IP or user agent.

This is usually done via a before_action in a controller, e.g.:

before_action :set_tracking_context

def set_tracking_context
  SolidusTracking.set_context(user_ip: request.remote_ip)
end

We should support this to allow for integrating additional plugins, such as Segment.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.