Skip to content
This repository has been archived by the owner on Aug 15, 2019. It is now read-only.

Commit

Permalink
Merge pull request #80 from botmetrics/n-1-path-analysis
Browse files Browse the repository at this point in the history
Show drilldown of path analysis
  • Loading branch information
arunthampi committed Jan 4, 2017
2 parents d7b94ba + bb5933a commit cb88117
Show file tree
Hide file tree
Showing 18 changed files with 790 additions and 104 deletions.
88 changes: 88 additions & 0 deletions app/assets/stylesheets/app.sass
Expand Up @@ -449,9 +449,97 @@ body.retention.index
font-family: Lucida Grande

body.paths
&.events
.events-row
padding-top: 40px
.user,
.bot
top: 15px
position: absolute
left: -140px
text-align: right
.name
margin: 0
.timestamp
color: #aaa
margin: 0
.bot
left: 360px
width: 160px
text-align: left

.talk-bubble
margin: 20px
position: relative
width: 300px
height: auto
min-height: 80px
border: 4px solid #3bafda
border-radius: 5px
-webkit-border-radius: 5px
-moz-border-radius: 5px
align: left
&.from-bot
align: right
p
padding: 10px 20px 10px 20px
font-size: 16px

.tri-right
&.right-in:before
content: ' '
position: absolute
width: 0
height: 0
left: auto
right: -20px
top: -4px
bottom: auto
border: 10px solid
border-color: #3bafda transparent transparent #3bafda
&.right-in:after
content: ' '
position: absolute
width: 0
height: 0
left: auto
right: -20px
top: -4px
bottom: auto
border: 12px solid
border-color: transparent transparent transparent transparent

&.left-in:before
content: ' '
position: absolute
width: 0
height: 0
left: -20px
right: auto
top: -4px
bottom: auto
border: 10px solid
border-color: #3bafda #3bafda transparent transparent
&.left-in:after
content: ' '
position: absolute
width: 0
height: 0
left: -20px
right: auto
top: -4px
bottom: auto
border: 12px solid
border-color: transparent transparent transparent transparent


.secondary-menu
.btn-group
margin-top: 15px
td.events-nav
i
font-size: 30px
opacity: 0.8
td.step-col
&.loading
background-image: image-url('ellipsis.gif')
Expand Down
31 changes: 30 additions & 1 deletion app/controllers/paths_controller.rb
Expand Up @@ -91,14 +91,43 @@ def insights

format.html do
@users = BotUser.where(id: insights[:group_by_user].keys).order(:id).paginate(page: params[:page], total_entries: @insights_by_user.size)
@users.each { |u| u.step_count = @insights_by_user[u.id] }
@users.each do |u|
u.step_count = @insights_by_user[u.id]
u.last_event = @funnel.events(u, step: @step, start_time: @start, end_time: @end, most_recent: true)
end

@show_steps = true

render :insights
end
end
end

def events
@funnel = @bot.funnels.find_by(uid: params[:id])
raise ActiveRecord::RecordNotFound if @funnel.blank?

if (@step = params[:step].to_i).present?
raise ActiveRecord::RecordNotFound if(@step < 0 || @step >= @funnel.dashboards.length - 1)
end

@bot_user = BotUser.where(id: params[:bot_user_id].to_i, bot_instance_id: @bot.instances.select(:id)).first
raise ActiveRecord::RecordNotFound if @bot_user.blank?

@start, @end = GetStartEnd.new(params[:start], params[:end], current_user.timezone).call
@events = @funnel.events(@bot_user, step: @step, start_time: @start, end_time: @end, most_recent: params[:most_recent] == 'true')

respond_to do |format|
format.json do
render json: { events: @events }
end

format.html do
render :events
end
end
end

protected
def update_dashboards_for_funnel!
dash_hash = {}
Expand Down
14 changes: 14 additions & 0 deletions app/helpers/paths_helper.rb
@@ -0,0 +1,14 @@
module PathsHelper
def formatted_event(event)
case event.event_type
when 'user-added' then 'User Signed Up'
when 'message'
if (attachments = event.event_attributes['attachments']).present?
"Message with Attachments: <code>#{attachments}</code>"
else
event.text.to_s
end
when 'messaging_postbacks' then "Clicked Button with payload: <code>#{event.event_attributes['payload']}</code>"
end
end
end
3 changes: 2 additions & 1 deletion app/models/bot_user.rb
Expand Up @@ -91,7 +91,8 @@ class BotUser < ActiveRecord::Base
where(created_at: min..max)
end

attr_accessor :step_count
attr_accessor :step_count, :last_event

store_accessor :user_attributes, :nickname, :email, :full_name, :first_name, :last_name, :gender, :timezone, :ref

def create_user_added_event
Expand Down
18 changes: 18 additions & 0 deletions app/models/event.rb
Expand Up @@ -129,6 +129,24 @@ def self.rollup!
Event.connection.execute("DROP FUNCTION IF EXISTS custom_append_to_rolledup_events_queue_on_update() CASCADE;")
end

def in_words
if self.event_type == 'message'
"Said '#{self.text}'"
else
case self.event_type
when 'messaging_postbacks'
payload = self.event_attributes['payload']
begin
payload = JSON.parse(payload)
rescue JSON::ParserError
return "Clicked Button: #{payload}"
end

"Clicked Button with user defined payload"
end
end
end

def created_at_string
self.created_at.to_s('%Y-%m-%d %H:%M:%S.%N')
end
Expand Down
91 changes: 71 additions & 20 deletions app/models/funnel.rb
Expand Up @@ -7,48 +7,99 @@ class Funnel < ActiveRecord::Base
belongs_to :bot
belongs_to :creator, class_name: 'User', foreign_key: 'user_id'

def events(user, step: 0, start_time: 1.week.ago, end_time: Time.current, most_recent: false)
dashboard1 = self.bot.dashboards.find_by(uid: self.dashboards[step].split(':').last)
dashboard2 = self.bot.dashboards.find_by(uid: self.dashboards[step + 1].split(':').last)

if dashboard1.custom?
first_event = dashboard1.raw_events.where(created_at: start_time..end_time, bot_user_id: user.id).order('created_at ASC').first
else
first_event_conditions = {
bot_user_id: user.id,
event_type: dashboard1.event_type,
created_at: start_time..end_time
}
first_event_conditions.merge!(dashboard1.query_options)
first_event = Event.where(first_event_conditions).order("created_at ASC").first
end

if dashboard2.custom?
last_event = dashboard2.raw_events.where(created_at: first_event.created_at..first_event.created_at+1.week, bot_user_id: user.id).order('created_at ASC').first
else
last_event_conditions = {
bot_user_id: user.id,
event_type: dashboard2.event_type,
created_at: first_event.created_at..first_event.created_at + 1.week
}
last_event_conditions.merge!(dashboard2.query_options)
last_event = Event.where(last_event_conditions).order("created_at ASC").first
end

return [] if first_event.blank? || last_event.blank?

if most_recent
events_relation = Event.where("id > ? AND id < ?", first_event.id, last_event.id).
where(bot_user_id: user.id).
where(is_for_bot: true)
else
events_relation = Event.where("id >= ? AND id <= ?", first_event.id, last_event.id).
where(bot_user_id: user.id)
end

most_recent ? events_relation.order("id DESC").first : events_relation.order("id ASC")
end

def insights(step: 0, start_time: 1.week.ago, end_time: Time.current)
result = {}

dashboard1 = self.bot.dashboards.find_by(uid: self.dashboards[step].split(':').last)
dashboard2 = self.bot.dashboards.find_by(uid: self.dashboards[step + 1].split(':').last)
exclude_other_dashboards = self.bot.dashboards.where(dashboard_type: ['messages', 'messages-from-bot', 'new-users']).pluck(:id)

dashboard_ids = [dashboard1.id, dashboard2.id] + exclude_other_dashboards
event_types_to_exclude = []
event_types_to_exclude << dashboard1.event_type if dashboard1.event_type.present?
event_types_to_exclude << dashboard2.event_type if dashboard2.event_type.present?

query = <<-SQL
SELECT rolledup_events.bot_user_id, rolledup_events.dashboard_id, SUM(rolledup_events.count)
FROM rolledup_events
SELECT events.id, events.bot_user_id, events.event_type, events.is_for_bot, dashboard_events.dashboard_id
FROM events
LEFT OUTER JOIN LATERAL (
SELECT e1.bot_user_id AS bot_user_id, dashboard_1_time, dashboard_2_time FROM
SELECT e1.bot_user_id AS bot_user_id, dashboard_1_time FROM
(
SELECT rolledup_events.bot_user_id, MIN(rolledup_events.created_at) AS dashboard_1_time
FROM rolledup_events
WHERE rolledup_events.dashboard_id = #{dashboard1.id}
AND rolledup_events.created_at BETWEEN '#{start_time.to_s(:db)}' AND '#{end_time.to_s(:db)}'
WHERE rolledup_events.created_at BETWEEN '#{start_time.to_s(:db)}' AND '#{end_time.to_s(:db)}'
AND rolledup_events.dashboard_id = #{dashboard1.id}
GROUP BY rolledup_events.bot_user_id
) e0 LEFT JOIN LATERAL (
) e0 INNER JOIN LATERAL (
SELECT rolledup_events.bot_user_id, MIN(rolledup_events.created_at) AS dashboard_2_time
FROM rolledup_events
WHERE bot_user_id = e0.bot_user_id
WHERE rolledup_events.created_at BETWEEN dashboard_1_time AND dashboard_1_time + INTERVAL '12 HOURS'
AND rolledup_events.dashboard_id = #{dashboard2.id}
AND bot_user_id = e0.bot_user_id
GROUP BY bot_user_id
HAVING MIN(rolledup_events.created_at) BETWEEN dashboard_1_time AND dashboard_1_time + INTERVAL '1 WEEK'
) e1 ON TRUE
) re ON TRUE
WHERE rolledup_events.bot_user_id = re.bot_user_id
AND rolledup_events.created_at BETWEEN re.dashboard_1_time AND re.dashboard_2_time
GROUP BY rolledup_events.bot_user_id, rolledup_events.dashboard_id
LEFT JOIN dashboard_events ON dashboard_events.event_id = events.id
WHERE events.bot_user_id = re.bot_user_id
AND events.created_at BETWEEN re.dashboard_1_time AND re.dashboard_1_time + INTERVAL '12 HOURS'
ORDER BY events.id ASC
SQL
intermediate_result = Funnel.connection.execute(query).to_a

start_counting, stop_counting = {}, {}

intermediate_result.each do |row|
dashboard_id = row['dashboard_id'].to_i
bot_user_id = row['bot_user_id'].to_i
sum = row['sum'].to_i
event_id, event_type, bot_user_id, dashboard_id = row['id'], row['event_type'], row['bot_user_id'].to_i, row['dashboard_id'].to_i
not_for_bot = row['is_for_bot'] == 'f'
is_for_bot = !!not_for_bot

start_counting[bot_user_id] = true if event_type == dashboard1.event_type || dashboard1.id == dashboard_id
stop_counting[bot_user_id] = true if event_type == dashboard2.event_type || dashboard2.id == dashboard_id

sum = dashboard_ids.index(dashboard_id).present? ? 0 : sum
result[bot_user_id] = result[bot_user_id].to_i + sum
if start_counting[bot_user_id] && !!!stop_counting[bot_user_id]
sum = not_for_bot ? 0 : 1
result[bot_user_id] = result[bot_user_id].to_i + sum
end
end

group_by_count = {}
Expand All @@ -57,7 +108,7 @@ def insights(step: 0, start_time: 1.week.ago, end_time: Time.current)
end
group_by_count = group_by_count.inject([]) do |arr, (k,v)|
arr << [k,v]
end.sort_by { |x| -x[1] }
end.sort_by { |x| -x[0] }

{group_by_user: result, group_by_count: group_by_count}
end
Expand All @@ -78,7 +129,7 @@ def conversion(start_time: 1.week.ago, end_time: Time.current)
SELECT rolledup_events.bot_user_id, 1 as dashboard_#{dashboard.uid}, MIN(rolledup_events.created_at) AS dashboard_#{dashboard.uid}_time
FROM rolledup_events
WHERE rolledup_events.dashboard_id = #{dashboard.id}
AND rolledup_events.created_at BETWEEN #{idx == 0 ? "'#{start_time.to_s(:db)}' AND '#{end_time.to_s(:db)}'" : "#{previous_dashboard_name}_time AND #{previous_dashboard_name}_time + INTERVAL '1 WEEK'"}
AND rolledup_events.created_at BETWEEN #{idx == 0 ? "'#{start_time.to_s(:db)}' AND '#{end_time.to_s(:db)}'" : "#{previous_dashboard_name}_time AND #{previous_dashboard_name}_time + INTERVAL '12 HOURS'"}
#{idx > 0 ? "AND bot_user_id = e#{idx-1}.bot_user_id" : ""}
GROUP BY rolledup_events.bot_user_id
#{idx < self.dashboards.length - 1 ? ") e#{idx} #{idx > 0 ? "ON TRUE" : ''} LEFT JOIN LATERAL (" : ""}
Expand Down
49 changes: 49 additions & 0 deletions app/views/paths/events.html.haml
@@ -0,0 +1,49 @@
.secondary-menu.row
.col-md-10.col-sm-10.col-xs-10
= link_to bot_paths_path(@bot), class: 'breadcrumb' do
%h2 All Paths
%span.breadcrumb-separator
\/
= link_to bot_path_path(@bot, @funnel, start: @start, end: @end), class: 'breadcrumb' do
%h2= @funnel.name
%span.breadcrumb-separator
\/
= link_to insights_bot_path_path(@bot, @funnel, step: @step, start: @start, end: @end), class: 'breadcrumb' do
%h2 Path Analysis
%span.breadcrumb-separator
\/
%h2
Interactions

.col-md-2.col-sm-2.col-xs-2.text-right
#report-range.pull-right
= icon('calendar')
%span
%b.caret

.container-fluid.retention-container
.row.events-row
.col-md-8.col-sm-8.col-xs-8.col-md-offset-2.col-sm-offset-2.col-xs-offset-2
- @events.each do |e|
- if e.is_from_bot?
.row
.col-md-6.col-sm-6.col-xs-6.col-md-offset-6.col-sm-offset-6.col-xs-offset-6
.bot
%p.name.strong= @bot.name
%p.timestamp= "#{e.created_at.in_time_zone(current_user.timezone)}"
.talk-bubble.tri-right.right-in.from-bot{id: "event-#{e.id}"}
%p= formatted_event(e).html_safe
- else
.row
.col-md-6.col-sm-6.col-xs-6
.user
%p.name.strong= "#{@bot_user.first_name} #{@bot_user.last_name}"
%p.timestamp= "#{e.created_at.in_time_zone(current_user.timezone)}"
.talk-bubble.tri-right.left-in.for-bot{id: "event-#{e.id}"}
%p= formatted_event(e).html_safe

- content_for :page_scripts do
:javascript
App.page = new App.FunnelsInsights(#{@bot.uid.to_json}, #{@funnel.uid.to_json}, #{@step.to_json}, #{@insights_by_count.to_json}, #{@start.to_json}, #{@end.to_json});
App.page.run();

0 comments on commit cb88117

Please sign in to comment.