Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ MAIL_HACKCLUB_TOKEN=replace_me

# Hack Club Account
HCA_CLIENT_ID=your_hackclub_account_client_id_here
HCA_CLIENT_SECRET=your_hackclub_account_secret_id_here
HCA_CLIENT_SECRET=your_hackclub_account_secret_id_here

# PostHog Analytics
POSTHOG_API_KEY=your_posthog_api_key_here
POSTHOG_HOST=https://us.i.posthog.com
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ gem "flamegraph"

gem "skylight"

# Analytics
gem "posthog-ruby"

gem "geocoder"

# Airtable syncing
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,8 @@ GEM
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
posthog-ruby (3.4.0)
concurrent-ruby (~> 1)
pp (0.6.3)
prettyprint
prettyprint (0.2.0)
Expand Down Expand Up @@ -636,6 +638,7 @@ DEPENDENCIES
oj
paper_trail
pg
posthog-ruby
propshaft
public_activity
puma (>= 5.0)
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/api/hackatime/v1/hackatime_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,8 @@ def handle_heartbeat(heartbeat_array)
Rails.logger.error("Error creating heartbeat: #{e.class.name} #{e.message}")
results << [ { error: e.message, type: e.class.name }, 422 ]
end

PosthogService.capture_once_per_day(@user, "heartbeat_sent", { heartbeat_count: heartbeat_array.size })
results
end

Expand Down
12 changes: 12 additions & 0 deletions app/controllers/sessions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ def hca_create
MigrateUserFromHackatimeJob.perform_later(@user.id)
end

PosthogService.identify(@user)
PosthogService.capture(@user, "user_signed_in", { method: "hca" })

if @user.created_at > 5.seconds.ago
session[:return_data] = { "url" => safe_return_url(params[:continue].presence) }
Rails.logger.info("Sessions return data: #{session[:return_data]}")
Expand Down Expand Up @@ -81,6 +84,9 @@ def slack_create
MigrateUserFromHackatimeJob.perform_later(@user.id)
end

PosthogService.identify(@user)
PosthogService.capture(@user, "user_signed_in", { method: "slack" })

state = JSON.parse(params[:state]) rescue {}
if state["close_window"]
redirect_to close_window_path
Expand Down Expand Up @@ -132,6 +138,7 @@ def github_create
@user = User.from_github_token(params[:code], redirect_uri, current_user)

if @user&.persisted?
PosthogService.capture(@user, "github_linked")
redirect_to my_settings_path, notice: "Successfully linked GitHub account!"
else
Rails.logger.error "Failed to link GitHub account"
Expand Down Expand Up @@ -250,6 +257,10 @@ def token
session[:user_id] = valid_token.user_id
session[:return_data] = valid_token.return_data || {}

user = User.find(valid_token.user_id)
PosthogService.identify(user)
PosthogService.capture(user, "user_signed_in", { method: "email" })

if valid_token.continue_param.present? && safe_return_url(valid_token.continue_param).present?
redirect_to safe_return_url(valid_token.continue_param), notice: "Successfully signed in!"
else
Expand Down Expand Up @@ -293,6 +304,7 @@ def stop_impersonating
end

def destroy
PosthogService.capture(session[:user_id], "user_signed_out") if session[:user_id]
session[:user_id] = nil
session[:impersonater_user_id] = nil
redirect_to root_path, notice: "Signed out!"
Expand Down
22 changes: 22 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def update
if @user.uses_slack_status?
@user.update_slack_status
end
PosthogService.capture(@user, "settings_updated", { fields: user_params.keys })
redirect_to is_own_settings? ? my_settings_path : settings_user_path(@user),
notice: "Settings updated successfully"
else
Expand All @@ -44,6 +45,7 @@ def rotate_api_key

new_api_key = @user.api_keys.create!(name: "Hackatime key")

PosthogService.capture(@user, "api_key_rotated")
render json: { token: new_api_key.token }, status: :ok
end
rescue => e
Expand All @@ -54,6 +56,12 @@ def rotate_api_key
def wakatime_setup
api_key = current_user&.api_keys&.last
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")
@current_user_api_key = api_key&.token
PosthogService.capture(current_user, "setup_started", { step: 1 })
end

def wakatime_setup_step_2
PosthogService.capture(current_user, "setup_step_viewed", { step: 2 })
setup_os = detect_setup_os(request.user_agent)

render inertia: "WakatimeSetup/Index", props: {
Comment on lines 57 to 67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The methods wakatime_setup_step_2 and wakatime_setup_step_4 are defined twice. The second definitions overwrite the first, causing PostHog analytics tracking calls to be skipped.
Severity: HIGH

Suggested Fix

Remove the duplicate method definitions for wakatime_setup_step_2 and wakatime_setup_step_4 in app/controllers/users_controller.rb. Consolidate the intended logic into a single definition for each method, ensuring the PostHog tracking calls are retained.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/controllers/users_controller.rb#L56-L67

Potential issue: In the `UsersController`, the methods `wakatime_setup_step_2` and
`wakatime_setup_step_4` are each defined twice. In Ruby, the second definition of a
method overwrites the first. As a result, the initial definitions, which contain logic
for capturing PostHog analytics events (`setup_step_viewed` and `setup_completed`), will
never be executed. The application will silently run the second, simpler definitions,
leading to a loss of analytics data for steps 2 and 4 of the Wakatime setup flow and
potentially incorrect rendering behavior.

Did we get this right? 👍 / 👎 to inform future reviews.

Expand All @@ -71,6 +79,20 @@ def wakatime_setup_step_2
def wakatime_setup_step_3
api_key = current_user&.api_keys&.last
api_key ||= current_user.api_keys.create!(name: "Wakatime API Key")

@current_user_api_key = api_key&.token
PosthogService.capture(current_user, "setup_step_viewed", { step: 3 })
end
Comment on lines 79 to +85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The wakatime_setup and wakatime_setup_step_3 controller actions are missing explicit render calls, which will cause a ActionView::MissingTemplate error at runtime.
Severity: CRITICAL

Suggested Fix

Add an explicit render call to both the wakatime_setup and wakatime_setup_step_3 methods. Based on the other actions, this should likely be render inertia: "WakatimeSetup/Index" and render inertia: "WakatimeSetup/Step3" respectively.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: app/controllers/users_controller.rb#L79-L85

Potential issue: The controller actions `wakatime_setup` and `wakatime_setup_step_3` in
`UsersController` lack an explicit `render` or `redirect_to` call. Since the controller
uses the Inertia.js framework, it expects an explicit `render inertia: "ComponentName"`
call to render the corresponding frontend component. Without it, Rails' default
rendering mechanism will fail to find a conventional template (e.g.,
`wakatime_setup.html.erb`), resulting in an `ActionView::MissingTemplate` error and
crashing the user's request when they access these setup steps.

Did we get this right? 👍 / 👎 to inform future reviews.


def wakatime_setup_step_4
@no_instruction_wording = [
"There is no step 4, lol.",
"There is no step 4, psych!",
"Tricked ya! There is no step 4.",
"There is no step 4, gotcha!"
].sample
PosthogService.capture(current_user, "setup_completed", { step: 4 })

editor = params[:editor]

render inertia: "WakatimeSetup/Step3", props: {
Expand Down
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,8 @@ def invalidate_activity_graph_cache

def create_signup_activity
create_activity :first_signup, owner: self
PosthogService.identify(self)
PosthogService.capture(self, "account_created", { source: "signup" })
end

def normalize_username
Expand Down
47 changes: 47 additions & 0 deletions app/services/posthog_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
class PosthogService
class << self
def capture(user_or_id, event, properties = {})
return unless $posthog

distinct_id = user_or_id.is_a?(User) ? user_or_id.id.to_s : user_or_id.to_s

$posthog.capture(
distinct_id: distinct_id,
event: event,
properties: properties
)
rescue => e
Rails.logger.error "PostHog capture error: #{e.message}"
end

def identify(user, properties = {})
return unless $posthog

$posthog.identify(
distinct_id: user.id.to_s,
properties: {
slack_uid: user.slack_uid,
username: user.username,
timezone: user.timezone,
country_code: user.country_code,
created_at: user.created_at&.iso8601,
admin_level: user.admin_level
}.merge(properties)
)
rescue => e
Rails.logger.error "PostHog identify error: #{e.message}"
end

def capture_once_per_day(user, event, properties = {})
return unless $posthog

cache_key = "posthog_daily:#{user.id}:#{event}:#{Date.current}"
return if Rails.cache.exist?(cache_key)

capture(user, event, properties)
Rails.cache.write(cache_key, true, expires_at: Date.current.end_of_day + 1.hour)
rescue => e
Rails.logger.error "PostHog capture_once_per_day error: #{e.message}"
end
end
end
20 changes: 20 additions & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,26 @@

<script defer data-domain="hackatime.hackclub.com" src="https://plausible.io/js/script.file-downloads.hash.js"></script>

<% if ENV['POSTHOG_API_KEY'].present? %>
<script>
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.crossOrigin="anonymous",p.async=!0,p.src=s.api_host.replace('.i.posthog.com','-assets.i.posthog.com')+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="init capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys getNextSurveyStep onSessionId setPersonProperties".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('<%= ENV['POSTHOG_API_KEY'] %>', {
api_host: '<%= ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') %>',
person_profiles: 'identified_only',
capture_pageview: true,
capture_pageleave: true,
autocapture: true
});
<% if current_user %>
posthog.identify('<%= current_user.id %>', {
slack_uid: '<%= current_user.slack_uid %>',
username: '<%= current_user.username %>',
timezone: '<%= current_user.timezone %>'
});
<% end %>
</script>
<% end %>

<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= stylesheet_link_tag :app %>
<%= stylesheet_link_tag 'tailwind', 'data-turbo-track': 'reload' %>
Expand Down
11 changes: 11 additions & 0 deletions config/initializers/posthog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require "posthog"

if ENV["POSTHOG_API_KEY"].present?
$posthog = PostHog::Client.new({
api_key: ENV["POSTHOG_API_KEY"],
host: ENV.fetch("POSTHOG_HOST", "https://us.i.posthog.com"),
on_error: proc { |status, msg| Rails.logger.error "PostHog error: #{status} - #{msg}" }
})
else
$posthog = nil
end