Compare commits

...

46 Commits

Author SHA1 Message Date
Seth Call 64a93dd42f support beta 2026-02-28 20:39:34 -06:00
Seth Call eb52813822 defaut beta to true 2026-02-21 08:56:52 -06:00
Seth Call 8613a03d00 ok fix the API more 2026-02-16 09:05:12 -06:00
Seth Call 8f0b8929ba ars apis 2026-02-15 22:04:17 -06:00
Seth Call 08e1c5274d track utm_id 2026-02-04 21:53:19 -06:00
Seth Call 5f347ccfac handle All 2026-02-04 21:31:23 -06:00
Seth Call ca2bf19b7a cleanup query 2026-02-04 20:58:16 -06:00
Seth Call fad1f9d6d9 Add paid = cpc on jammers cohots 2026-02-04 19:53:00 -06:00
Seth Call 51838fb413 Remove test drive 2026-02-03 19:05:12 -06:00
Seth Call 45cb401112 Controls for user source 2026-02-01 14:15:41 -06:00
Seth Call 9078515984 user source update 2026-01-31 12:20:37 -06:00
Seth Call 52ae83e2f4 Addtocart instead 2026-01-31 12:15:26 -06:00
Seth Call b454cf9ead AddToCart 2026-01-28 20:04:40 -06:00
Seth Call 65f5624ff9 add array support for facebook ad source 2026-01-24 00:02:44 -06:00
Seth Call e82450dfe9 Support more utm tracking 2026-01-23 23:17:35 -06:00
Seth Call f1992eaa78 build this 2026-01-22 22:36:22 -06:00
Seth Call bdeecc76c6 fix reporting 2026-01-22 21:21:17 -06:00
Seth Call 8c8024c12b event test code 2026-01-19 13:48:26 -06:00
Seth Call c183af3d6b remove rails logger ref 2026-01-19 13:21:26 -06:00
Seth Call ab424c21d2 use puts 2026-01-19 12:49:37 -06:00
Seth Call 753f35b24d click signup redirects to the right place in jam-ui 2026-01-19 12:34:30 -06:00
Seth Call 624853c868 Store UTM cookies 2026-01-16 22:15:36 -06:00
Seth Call 71d8571eb9 JKLayout adding Meta Tracking 2026-01-16 16:44:46 -06:00
Seth Call fae16c7483 better logging on CAPI 2026-01-15 08:00:34 -06:00
Seth Call 253a6c566b Bettor logging 2026-01-15 07:59:27 -06:00
Seth Call ed561be4b5 meta_tracking included 2026-01-14 19:56:42 -06:00
Seth Call eb69640667 fix utm source 2026-01-14 14:54:56 -06:00
Seth Call 9b0a9d1f32 cleanup around app-config 2026-01-14 14:42:22 -06:00
Seth Call e1cc5483a2 facebook pixel id and access token 2026-01-14 12:17:39 -06:00
Seth Call 73d2a7a020 more changes for event rework 2026-01-14 10:30:33 -06:00
Seth Call eb298d6859 fix 2026-01-14 09:20:57 -06:00
Seth Call 93c4154648 add capi transmitter and missing event type 2026-01-14 08:58:37 -06:00
Seth Call fbd871d204 reviewing 2026-01-14 08:52:19 -06:00
Seth Call 0d82f6ee16 allow video tokens to last a long time 2026-01-08 06:03:58 -06:00
Seth Call 1279b16ec0 Update manifest maker 2025-10-26 14:33:00 -05:00
Seth Call e3cff0a825 Uncomment TrialReminders 2025-10-18 17:40:46 -05:00
Nuwan Chaturanga 6a6e4cde09 Merged in fix_plg_email_delivery_timeing_issue (pull request #70)
fix PLG email timing

Approved-by: Seth Call
2025-10-17 13:16:50 +00:00
Seth Call 1dd15fb0aa Add tests for all PLG emails 2025-10-17 08:15:46 -05:00
Seth Call 9282369e54 VRFS-5690 - tag jamtrack and jammers separetly on admin report - fix jammers view 2025-10-13 15:23:01 -05:00
Seth Call 828191d683 VRFS-5690 - tag jamtrack and jammers separetly on admin report - fix jammers view 2025-10-13 15:16:41 -05:00
Seth Call c0031cfe3d VRFS-5690 - tag jamtrack and jammers separetly on admin report 2025-10-13 14:33:45 -05:00
Seth Call 9b17546082 omit trial send reminders for now 2025-10-13 12:02:44 -05:00
Seth Call eed3d51f4b VRFS-5691 - fix both the jamtrack flow and the inability to sign out - take 2 2025-10-13 11:44:17 -05:00
Seth Call 4eac68b645 VRFS-5691 - fix both the jamtrack flow and the inability to sign out 2025-10-12 11:55:50 -05:00
Nuwan fe6157e8cf ensure not to send email 2 and 3 tighltly behind email 1 in this sequence 2025-09-30 13:14:39 +05:30
Nuwan 675bf2b69c fix PLG email timing
do not send emails if the date has passed
2025-09-22 00:07:16 +05:30
57 changed files with 1177 additions and 36085 deletions

View File

View File

@ -0,0 +1,185 @@
class Spacer
def self.spacer(val, row)
percentage = ((val * 100) / row.total.to_f).round(1).to_s
('%-5.5s' % percentage).gsub(' ', ' ') + '% - ' + val.to_s
end
end
=begin
select
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
select first_name, last_name, email
from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-04-01'
AND first_music_session_at is NULL;
=end
ActiveAdmin.register_page "Jammers Subscription Cohorts" do
menu :parent => 'Reports'
content :title => "Jammers Subscription Cohorts" do
filter_type = params[:filter_type] || 'All'
filter_campaign = params[:filter_campaign]
filter_campaign_id = params[:filter_campaign_id]
filter_ad_set = params[:filter_ad_set]
filter_ad_name = params[:filter_ad_name]
campaigns = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_utm_campaign).compact.sort
campaign_ids = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_id).compact.sort
ad_sets = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_term).compact.sort
ad_names = User.where("origin_utm_medium = 'cpc'").distinct.pluck(:origin_content).compact.sort
div style: "margin-bottom: 20px; padding: 10px; background-color: #f4f4f4; border-radius: 4px;" do
form action: admin_jammers_subscription_cohorts_path, method: :get do
span "Source: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_type', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: 'All', selected: filter_type == 'All'
option "Organic", value: 'Organic', selected: filter_type == 'Organic'
option "Advertising", value: 'Advertising', selected: filter_type == 'Advertising'
end
if filter_type == 'Advertising'
div style: "margin-top: 10px;" do
span "Campaign Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign == 'NULL'
campaigns.each do |c|
option c, value: c, selected: filter_campaign == c
end
end
end
div style: "margin-top: 10px;" do
span "Campaign ID: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_campaign_id', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_campaign_id == 'NULL'
campaign_ids.each do |c|
option c, value: c, selected: filter_campaign_id == c
end
end
end
div style: "margin-top: 10px;" do
span "Ad Set: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_set', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_set == 'NULL'
ad_sets.each do |c|
option c, value: c, selected: filter_ad_set == c
end
end
div style: "margin-top: 10px;" do
span "Ad Name: ", style: "font-weight: bold; margin-right: 5px;"
select name: 'filter_ad_name', onchange: 'this.form.submit()', style: "margin-right: 15px;" do
option "All", value: ''
option "Null", value: 'NULL', selected: filter_ad_name == 'NULL'
ad_names.each do |c|
option c, value: c, selected: filter_ad_name == c
end
end
end
end
end
noscript { input type: :submit, value: "Filter" }
end
end
h2 "Users Grouped By Month as Paying Subscribers"
query = User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total,
count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client,
count(first_certified_gear_at) as ftue,
count(first_music_session_at) as any_session,
count(first_real_music_session_at) as real_session,
count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited,
count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed,
count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{LEFT JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON TRUE })
if filter_type == 'Organic'
query = query.where("users.origin_utm_source = 'organic'")
elsif filter_type == 'Advertising'
query = query.where("origin_utm_medium = 'cpc'")
if filter_campaign.present?
if filter_campaign == 'NULL'
query = query.where("users.origin_utm_campaign IS NULL")
elsif filter_campaign != 'All'
query = query.where("users.origin_utm_campaign = ?", filter_campaign)
end
end
if filter_campaign_id.present?
if filter_campaign_id == 'NULL'
query = query.where("users.origin_id IS NULL")
elsif filter_campaign_id != 'All'
query = query.where("users.origin_id = ?", filter_campaign_id)
end
end
if filter_ad_set.present?
if filter_ad_set == 'NULL'
query = query.where("users.origin_term IS NULL")
elsif filter_ad_set != 'All'
query = query.where("users.origin_term = ?", filter_ad_set)
end
end
if filter_ad_name.present?
if filter_ad_name == 'NULL'
query = query.where("users.origin_content IS NULL")
elsif filter_ad_name != 'All'
query = query.where("users.origin_content = ?", filter_ad_name)
end
end
end
table_for query.group("date_trunc('month', users.created_at)")
.where("j.created_at IS NULL OR (j.created_at - users.created_at) >= INTERVAL '2 hours'")
.order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
column "Downloaded", Proc.new { |user| raw(Spacer.spacer(user.downloaded, user)) }
column "Ran Client", Proc.new { |user| raw(Spacer.spacer(user.ran_client, user)) }
column "FTUE", Proc.new { |user| raw(Spacer.spacer(user.ftue, user)) }
column "Any Session", Proc.new { |user| raw(Spacer.spacer(user.any_session, user)) }
column "2+ Session", Proc.new { |user| raw(Spacer.spacer(user.real_session, user)) }
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end
end
end

View File

@ -25,13 +25,12 @@ from users where users.created_at >= '2024-11-01' AND users.created_at < '2025-0
=end =end
ActiveAdmin.register_page "Subscription Cohorts" do ActiveAdmin.register_page "JamTrack Subscription Cohorts" do
menu :parent => 'Reports' menu :parent => 'Reports'
content :title => "JamTrack Subscription Cohorts" do
content :title => "Subscription Cohorts" do
h2 "Users Grouped By Month as Paying Subscribers" h2 "Users Grouped By Month as Paying Subscribers"
table_for User.select(%Q{date_trunc('month', created_at) as month, table_for User.select(%Q{date_trunc('month', users.created_at) as month,
count(id) as total, count(id) as total,
count(first_downloaded_client_at) as downloaded, count(first_downloaded_client_at) as downloaded,
count(first_ran_client_at) as ran_client, count(first_ran_client_at) as ran_client,
@ -41,8 +40,21 @@ ActiveAdmin.register_page "Subscription Cohorts" do
count(first_good_music_session_at) as good_session, count(first_good_music_session_at) as good_session,
count(first_invited_at) as invited, count(first_invited_at) as invited,
count(first_friended_at) as friended, count(first_friended_at) as friended,
count(first_subscribed_at) as subscribed count(first_subscribed_at) as subscribed,
}).group("date_trunc('month', created_at)").order("date_trunc('month', created_at) DESC") do |row| count(first_played_jamtrack_at) as played_jamtrack
})
.joins(%Q{INNER JOIN LATERAL (
SELECT
j.created_at
FROM
jam_track_rights as j
WHERE
j.user_id = users.id
ORDER BY
j.created_at
LIMIT 1 -- Select only that single row
) j ON (j.created_at - users.created_at) < INTERVAL '2 hours' })
.group("date_trunc('month', users.created_at)").order("date_trunc('month', users.created_at) DESC") do |row|
column "Month", Proc.new { |user| user.month.strftime('%B %Y') } column "Month", Proc.new { |user| user.month.strftime('%B %Y') }
column "Total", :total column "Total", :total
column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) } column "Subscribed", Proc.new { |user| raw(Spacer.spacer(user.subscribed, user)) }
@ -54,6 +66,7 @@ ActiveAdmin.register_page "Subscription Cohorts" do
column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) } column "Good Session", Proc.new { |user| raw(Spacer.spacer(user.good_session, user)) }
column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) } column "Invited", Proc.new { |user| raw(Spacer.spacer(user.invited, user)) }
column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) } column "Friended", Proc.new { |user| raw(Spacer.spacer(user.friended, user)) }
column "Played JT", Proc.new { |user| raw(Spacer.spacer(user.played_jamtrack, user)) }
end end
end end

View File

@ -6,15 +6,18 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
config.batch_actions = false config.batch_actions = false
config.clear_action_items! config.clear_action_items!
config.filters = false config.filters = false
config.per_page = 250
scope("Most Recent First", default: true) { |scope| scope.unscoped.order('created_at desc')} scope("Paid", default: true) { |scope| scope.unscoped.where(:origin_utm_medium => 'cpc').order('created_at desc') }
scope("Inorganic Source") { |scope| scope.unscoped.where("origin_utm_source != 'organic' OR origin_utm_source IS NULL").order('created_at desc') }
scope("Include Organic") { |scope| scope.unscoped.order('created_at desc') }
index do index do
column "Email" do |user| column "Email" do |user|
user.email user.email
end end
column "Bought TestDrive" do |user| column "Signup (CST)" do |user|
!user.most_recent_test_drive_purchase.nil? ? "Yes" : "No" user.created_at.in_time_zone("Central Time (US & Canada)")
end end
column "UTM Source" do |user| column "UTM Source" do |user|
user.origin_utm_source user.origin_utm_source
@ -25,8 +28,23 @@ ActiveAdmin.register JamRuby::User, :as => 'UserSource' do
column "UTM Campaign" do |user| column "UTM Campaign" do |user|
user.origin_utm_campaign user.origin_utm_campaign
end end
column "UTM ID" do |user|
user.origin_id
end
column "UTM Term" do |user|
user.origin_term
end
column "UTM Content" do |user|
user.origin_content
end
column "Referrer" do |user| column "Referrer" do |user|
user.origin_referrer user.origin_referrer
end end
column "FB Click ID" do |user|
user.facebook_click_id
end
column "FB Browser ID" do |user|
user.facebook_browser_id
end
end end
end end

View File

@ -2,55 +2,97 @@ class ArsesController < ApplicationController
respond_to :json respond_to :json
# create or update a client_artifact row def index
def get_or_create if params[:code] != Rails.application.config.data_dump_code
name = params[:name] render :json => {error: "Unauthorized"}, :status => 401
provider = params[:provider] return
active = params[:active]
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
puts "TOPOLOGY #{topology}"
if ars_id
ars = Ars.find_by_id_int(ars_id)
end
if ars.nil?
ars = Ars.new
ars.name = name
ars.id_int = ars_id if !ars_id.nil?
end end
ars.provider = provider @arses = JamRuby::Ars.all
ars.active = active render :json => @arses
ars.ip = ip end
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save
@ars = ars def update
unless @ars.errors.any? if params[:code] != Rails.application.config.data_dump_code
if ars_id.nil? render :json => {error: "Unauthorized"}, :status => 401
ars.reload return
ars_id = ars.id_int end
begin
# Primary ID lookup
@ars = JamRuby::Ars.find_by_id(params[:id])
# Explicit secondary lookups if primary ID fails
@ars ||= JamRuby::Ars.find_by_id_int(params[:id_int]) if params[:id_int]
@ars ||= JamRuby::Ars.find_by_name(params[:name]) if params[:name]
if @ars.nil?
render :json => {error: "Not Found"}, :status => 404
return
end end
@ars = Ars.find_by_id_int(ars_id) allowed = [:password, :username, :active, :beta, :name, :provider, :id_int, :ip, :port, :continent, :country, :city, :subdivision, :latitude, :longitude]
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, ip: @ars.ip}, :status => :ok
else
response.status = :unprocessable_entity
respond_with @ars
end
update_hash = {}
allowed.each do |attr|
update_hash[attr] = params[attr] if params.has_key?(attr)
end
if @ars.update_attributes(update_hash, as: :admin)
render :json => @ars, :status => :ok
else
render :json => @ars.errors, :status => :unprocessable_entity
end
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
end
end
# create or update a client_artifact row
def get_or_create
begin
name = params[:name]
provider = params[:provider]
active = params[:active]
beta = params.has_key?(:beta) ? params[:beta] : true
ip = params[:ip]
username = params[:username]
password = params[:password]
topology = params[:topology]
ars_id = params[:ars_id]
# Explicit field-based lookups
ars = nil
ars = JamRuby::Ars.find_by_id_int(ars_id) if ars_id
ars ||= JamRuby::Ars.find_by_name(name) if name
if ars.nil?
ars = JamRuby::Ars.new
ars.name = name
end
ars.id_int = ars_id if !ars_id.nil?
ars.provider = provider
ars.active = active
ars.beta = params[:beta]
ars.beta = beta
ars.ip = ip
ars.password = password
ars.username = username
if topology
ars.city = topology['city']
ars.country = topology['country']
ars.continent = topology['continent']
ars.latitude = topology['latitude']
ars.longitude = topology['longitude']
ars.subdivision = topology['subdivision']
end
ars.save!
@ars = ars
render :json => {id_int: @ars.id_int, id: @ars.id, name: @ars.name, provider: @ars.provider, active: @ars.active, beta: @ars.beta, ip: @ars.ip}, :status => :ok
rescue => e
render :json => {error: e.message, backtrace: e.backtrace.first(5)}, :status => 500
end
end end
end end

View File

@ -44,6 +44,8 @@ JamAdmin::Application.routes.draw do
match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv' match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv'
match '/api/arses/register' => 'arses#get_or_create', :via => :post match '/api/arses/register' => 'arses#get_or_create', :via => :post
match '/api/arses' => 'arses#index', :via => :get
match '/api/arses/:id' => 'arses#update', :via => :post
mount Resque::Server.new, :at => "/resque" mount Resque::Server.new, :at => "/resque"

View File

@ -38,3 +38,19 @@ npm run start
This will open it in a borwser window at http://beta.jamkazam.local:3000. Of course for it to work you also need Rails (web) app and websocket app (websocket-gateway) running. This will open it in a borwser window at http://beta.jamkazam.local:3000. Of course for it to work you also need Rails (web) app and websocket app (websocket-gateway) running.
## Working with JamTracks
if you have the latest from develop, you can go:
```
cd cicd
npm install
./export_personal_jamtracks.sh
./generate.js
open http://beta.jamkazam.local:4000/backing-tracks/ac-dc/back-in-black.html
```
You can also do none of the above, and go straight to:
http://beta.jamkazam.local:4000/public/backing-tracks/ac-dc/back-in-black
I tried to make it so the SPA has 'secret routes' to these pages, which is convenient for us dev & testing
but also tried to make it convenient to run the cicd approach of actually generating separate pages for each landing page (which is what those 5 steps cover)

View File

@ -57,20 +57,26 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
useEffect(() => { useEffect(() => {
const addJamTrackToCart = async () => { const addJamTrackToCart = async () => {
try{ try {
await addToCart(jamTrack); await addToCart(jamTrack);
}catch(error){ } catch (error) {
console.error(error); console.error(error);
} }
} }
console.log('currentUser', currentUser);
console.log('jamTrack', jamTrack);
console.log('jamTrackArtistName', jamTrackArtistName);
if (currentUser) { if (currentUser) {
if(jamTrack){ if (jamTrack) {
console.log('adding jamtrack to cart');
addJamTrackToCart(); addJamTrackToCart();
}else if(jamTrackArtistName){ } else if (jamTrackArtistName) {
console.log('redirecting to jamtracks artist landing');
history.push(`/jamtracks?artist=${jamTrackArtistName}`); history.push(`/jamtracks?artist=${jamTrackArtistName}`);
}else{ } else {
history.push('/public/downloads'); console.log('redirecting to profile');
history.push('/profile');
} }
} }
}, [currentUser, jamTrack, jamTrackArtistName]); }, [currentUser, jamTrack, jamTrackArtistName]);
@ -140,7 +146,7 @@ const JKRegistrationForm = ({ hasLabel, jamTrack, jamTrackArtistName }) => {
<FormGroup> <FormGroup>
<Button color="primary" block className="mt-3" disabled={isDisabled}> <Button color="primary" block className="mt-3" disabled={isDisabled}>
{t('signup')} {t('signup')}
</Button> </Button>
</FormGroup> </FormGroup>

View File

@ -33,7 +33,9 @@ const Registration = () => {
try { try {
const response = await getJamTrackBySlug({ slug }); const response = await getJamTrackBySlug({ slug });
const jamTrack = await response.json(); const jamTrack = await response.json();
console.log('jamTrack', jamTrack);
setJamTrack(jamTrack); setJamTrack(jamTrack);
setJamTrackArtistName(jamTrack.original_artist);
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -1,11 +1,27 @@
import React, { Fragment } from 'react'; import React, { Fragment, useEffect } from 'react';
import { Col, Row } from 'reactstrap'; import { Col, Row } from 'reactstrap';
import { Link } from 'react-router-dom'; import { Link, useHistory, useLocation } from 'react-router-dom';
import LoginForm from '../LoginForm'; import LoginForm from '../LoginForm';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuth } from '../../../context/UserAuth';
const Login = () => { const Login = () => {
const { t } = useTranslation('auth'); const { t } = useTranslation('auth');
const { isAuthenticated, isLoading } = useAuth();
const history = useHistory();
const location = useLocation();
useEffect(() => {
if (!isLoading && isAuthenticated) {
const { from } = location.state || { from: { pathname: '/profile' } };
const finalDestination = from.pathname === '/' ? { pathname: '/profile' } : from;
history.replace(finalDestination);
}
}, [isAuthenticated, isLoading, history, location]);
if (isLoading || isAuthenticated) {
return null; // Or a loading spinner
}
return ( return (
<Fragment> <Fragment>

View File

@ -23,7 +23,7 @@ const awesome9 = '/img/landing/jamtracks/Top 10 Image - Number 9.webp';
const BodyComponent = ({ const BodyComponent = ({
id = "1", id = "1",
plan_code = "jamtrack-acdc-backinblack", plan_code = "jamtrack-acdc-backinblack",
slug = "ac-dc-back-in-black", slug = "acdc-back-in-black",
artist = "AC/DC (defaulted)", artist = "AC/DC (defaulted)",
song = "Back in Black (defaulted)", song = "Back in Black (defaulted)",
provided_jam_track = null provided_jam_track = null

View File

@ -23,12 +23,24 @@ const ProfileDropdown = () => {
const handleLogout = async event => { const handleLogout = async event => {
event.preventDefault(); event.preventDefault();
const cookieDomain = `.${process.env.REACT_APP_ORIGIN}`;
console.log('handleLogout: cookie: ', cookieDomain);
removeCookie('remember_token', { removeCookie('remember_token', {
domain: `.${process.env.REACT_APP_ORIGIN}` domain: cookieDomain,
path: '/'
}); });
setCurrentUser(null); setCurrentUser(null);
// This will cause the server to remove the cookie TOO (overkill),
// but this code has a side effect because something, somewhere,
// client-side history pushes to /auth/login because of this function.
// ...
await logout(); await logout();
// But I don't want to count on that side effect (defensive against future changes), so I force a page location hit here
window.location.href = "/auth/login"; window.location.href = "/auth/login";
// And here's the final bit; force a full page refresh ANYWAY.
// This is the only way we seem to really clear
// 'login state' cleanly at this point
window.location.reload();
}; };
return ( return (

View File

@ -0,0 +1,99 @@
/**
* meta_tracking.js
* A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies.
*
* Logic adapted from web/app/assets/javascripts/meta_tracking.js for React environment.
* - Checks URL for `fbclid` and sets `_fbc` cookie.
* - Checks for `_fbp` cookie; if missing, generates and sets it.
*/
const MetaTracking = {
init: function () {
const location = window.location;
this.handleFbc(location.search);
this.handleFbp();
this.handleUtm(location.search);
},
// 1. Parsing and storing _fbc (Click ID)
handleFbc: function (searchParams) {
const fbclid = this.getQueryParam('fbclid', searchParams);
if (fbclid) {
const version = 'fb';
const subdomainIndex = 1; // 1 = example.com
const creationTime = new Date().getTime(); // Unix timestamp in ms
// Format: fb.1.timestamp.id
const fbcValue = `${version}.${subdomainIndex}.${creationTime}.${fbclid}`;
this.setCookie('_fbc', fbcValue, 90);
}
},
handleUtm: function (searchParams) {
if (!searchParams) return;
const query = searchParams.substring(1);
const vars = query.split('&');
vars.forEach(v => {
const pair = v.split('=');
if (pair.length === 2) {
const key = decodeURIComponent(pair[0]);
const value = decodeURIComponent(pair[1]);
if (key.indexOf('utm_') === 0) {
this.setCookie(key, value, 90);
}
}
});
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {
const version = 'fb';
const subdomainIndex = 1;
const creationTime = new Date().getTime();
const randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number
// Format: fb.1.timestamp.randomDigits
const fbpValue = `${version}.${subdomainIndex}.${creationTime}.${randomInt}`;
this.setCookie('_fbp', fbpValue, 90);
}
},
// Helper: Get query param by name
getQueryParam: function (name, search) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
const regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
const results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
// Helper: Set cookie
setCookie: function (name, value, days) {
let expires = "";
if (days) {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
// Ensure path is root and domain is included if needed (defaults to current host)
document.cookie = name + "=" + (value || "") + expires + "; path=/";
},
// Helper: Get cookie
getCookie: function (name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
};
export default MetaTracking;

View File

@ -500,9 +500,7 @@ export const getJamTrackPublic = options => {
export const getJamTrackBySlug = options => { export const getJamTrackBySlug = options => {
const { slug } = options; const { slug } = options;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
apiFetch(`/jamtracks/${slug}`, { apiFetch(`/jamtracks/public/${slug}`)
baseUrl: process.env.REACT_APP_CLIENT_BASE_URL
})
.then(response => resolve(response)) .then(response => resolve(response))
.catch(error => reject(error)); .catch(error => reject(error));
}); });

View File

@ -1,4 +1,4 @@
import React, {useEffect} from 'react'; import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import { toast, ToastContainer } from 'react-toastify'; import { toast, ToastContainer } from 'react-toastify';
import { CloseButton, Fade } from '../components/common/Toast'; import { CloseButton, Fade } from '../components/common/Toast';
@ -8,6 +8,7 @@ import ErrorLayout from './ErrorLayout';
import BuildMeta from "./JKBuildMeta"; import BuildMeta from "./JKBuildMeta";
import loadable from '@loadable/component'; import loadable from '@loadable/component';
import MetaTracking from "../helpers/MetaTracking";
const AuthBasicLayout = loadable(() => import('./JKAuthBasicLayout')); const AuthBasicLayout = loadable(() => import('./JKAuthBasicLayout'));
const PublicLayout = loadable(() => import('./JKPublicLayout')); const PublicLayout = loadable(() => import('./JKPublicLayout'));
@ -15,6 +16,7 @@ const Layout = () => {
useEffect(() => { useEffect(() => {
AuthBasicLayout.preload(); AuthBasicLayout.preload();
PublicLayout.preload(); PublicLayout.preload();
MetaTracking.init();
//see if there is affiliate in query string and save it as cookie //see if there is affiliate in query string and save it as cookie
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -28,7 +30,7 @@ const Layout = () => {
}, []); }, []);
return ( return (
<> <>
<Switch> <Switch>
<Route path="/errors" component={ErrorLayout} /> <Route path="/errors" component={ErrorLayout} />
<Route path="/auth" component={AuthBasicLayout} /> <Route path="/auth" component={AuthBasicLayout} />
@ -37,7 +39,7 @@ const Layout = () => {
</Switch> </Switch>
<ToastContainer transition={Fade} closeButton={<CloseButton />} position={toast.POSITION.BOTTOM_RIGHT} /> <ToastContainer transition={Fade} closeButton={<CloseButton />} position={toast.POSITION.BOTTOM_RIGHT} />
<BuildMeta /> <BuildMeta />
</> </>
); );
}; };

View File

@ -3,7 +3,14 @@
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP" execute "ALTER TABLE users ADD COLUMN trial_expires_reminder1_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP" execute "ALTER TABLE users ADD COLUMN trial_expires_reminder2_sent_at TIMESTAMP"
execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP" execute "ALTER TABLE users ADD COLUMN trial_expires_reminder3_sent_at TIMESTAMP"
end
# slide in some more production indexes
execute "CREATE INDEX index_users_on_first_music_session_at ON users USING btree (first_music_session_at)"
# subscription_sync_code
execute "CREATE INDEX index_users_on_subscription_sync_code ON users USING btree (subscription_sync_code)"
# first_certified_gear_at
execute "CREATE INDEX index_users_on_first_certified_gear_at ON users USING btree (first_certified_gear_at)"
end
def self.down def self.down
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at" execute "ALTER TABLE users DROP COLUMN trial_expires_reminder1_sent_at"
execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at" execute "ALTER TABLE users DROP COLUMN trial_expires_reminder2_sent_at"

View File

@ -0,0 +1,11 @@
class AddFacebookTrackingToUsers < ActiveRecord::Migration
def up
execute "ALTER TABLE users ADD COLUMN facebook_click_id varchar(1000)"
execute "ALTER TABLE users ADD COLUMN facebook_browser_id varchar(1000)"
end
def down
execute "ALTER TABLE users DROP COLUMN facebook_click_id"
execute "ALTER TABLE users DROP COLUMN facebook_browser_id"
end
end

View File

@ -0,0 +1,31 @@
class AddExtendedUtmToUsers < ActiveRecord::Migration[5.0]
def up
execute <<-SQL
ALTER TABLE users ADD COLUMN origin_id character varying;
ALTER TABLE users ADD COLUMN origin_term character varying;
ALTER TABLE users ADD COLUMN origin_content character varying;
CREATE INDEX index_users_on_origin_id ON users (origin_id);
CREATE INDEX index_users_on_origin_term ON users (origin_term);
CREATE INDEX index_users_on_origin_content ON users (origin_content);
CREATE INDEX index_users_on_origin_utm_source ON users (origin_utm_source);
CREATE INDEX index_users_on_origin_utm_medium ON users (origin_utm_medium);
SQL
end
def down
execute <<-SQL
DROP INDEX IF EXISTS index_users_on_origin_utm_medium;
DROP INDEX IF EXISTS index_users_on_origin_utm_source;
DROP INDEX IF EXISTS index_users_on_origin_content;
DROP INDEX IF EXISTS index_users_on_origin_term;
DROP INDEX IF EXISTS index_users_on_origin_id;
ALTER TABLE users DROP COLUMN IF EXISTS origin_content;
ALTER TABLE users DROP COLUMN IF EXISTS origin_term;
ALTER TABLE users DROP COLUMN IF EXISTS origin_id;
SQL
end
end

View File

@ -56,6 +56,7 @@ require "jam_ruby/lib/em_helper"
require "jam_ruby/lib/nav" require "jam_ruby/lib/nav"
require "jam_ruby/lib/html_sanitize" require "jam_ruby/lib/html_sanitize"
require "jam_ruby/lib/guitar_center" require "jam_ruby/lib/guitar_center"
require "jam_ruby/lib/capi_transmitter"
require "jam_ruby/subscription_definitions" require "jam_ruby/subscription_definitions"
require "jam_ruby/resque/resque_jam_error" require "jam_ruby/resque/resque_jam_error"
require "jam_ruby/resque/resque_hooks" require "jam_ruby/resque/resque_hooks"

View File

@ -436,7 +436,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id]) sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder2" sendgrid_unique_args :type => "profile_complete_reminder2"
mail(:to => user.email, :subject => "Take 2 minutes to fill out your JamKazam profile now") do |format| mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder2.subject')) do |format|
format.text format.text
format.html { render layout: "user_mailer_beta" } format.html { render layout: "user_mailer_beta" }
end end
@ -448,7 +448,7 @@ module JamRuby
sendgrid_substitute('@USERID', [user.id]) sendgrid_substitute('@USERID', [user.id])
sendgrid_unique_args :type => "profile_complete_reminder3" sendgrid_unique_args :type => "profile_complete_reminder3"
mail(:to => user.email, :subject => "Last reminder to update your JamKazam profile") do |format| mail(:to => user.email, :subject => I18n.t('user_mailer.profile_complete_reminder3.subject')) do |format|
format.text format.text
format.html { render layout: "user_mailer_beta" } format.html { render layout: "user_mailer_beta" }
end end

View File

@ -0,0 +1,86 @@
require 'net/http'
require 'uri'
require 'json'
class CapiTransmitter
def self.send_event(event_name, user, custom_data = {}, event_source_url = nil, test_event_code = nil)
begin
puts("CapiTransmitter: Sending event #{event_name} for User #{user.id}")
pixel_id = APP_CONFIG.facebook_pixel_id
access_token = APP_CONFIG.facebook_access_token
if pixel_id.blank? || access_token.blank?
puts("CapiTransmitter: Missing Facebook Pixel ID or Access Token. Skipping event #{event_name}.")
return
end
# Construct the User Data object
user_data = {
# client_ip_address: user.current_sign_in_ip,
# client_user_agent: user.user_agent # Note: User model might not have user_agent stored directly, might need to pass it or rely on what's available
}
if !user.facebook_click_id.present? ||
!APP_CONFIG.facebook_ad_source.include?(user.origin_utm_source)
return
end
# Enhance user data with hashed PII if available
# Facebook requires SHA256 hashing for PII
# For now, we rely on click_id and browser_id as primary keys if available
user_data[:fbc] = user.facebook_click_id if user.facebook_click_id.present?
user_data[:fbp] = user.facebook_browser_id if user.facebook_browser_id.present?
# If we have email/phone, we should hash them. But for now, let's stick to the IDs.
# Ideally we should hash email if we have it.
if user.email.present?
user_data[:em] = Digest::SHA256.hexdigest(user.email.downcase)
end
payload = {
data: [
{
event_name: event_name,
event_time: Time.now.to_i,
action_source: "website",
user_data: user_data,
custom_data: custom_data,
event_source_url: event_source_url
}
]
}
if test_event_code.present?
payload[:test_event_code] = test_event_code
end
url = URI.parse("https://graph.facebook.com/v19.0/#{pixel_id}/events?access_token=#{access_token}")
http = Net::HTTP.new(url.host, url.port)
if APP_CONFIG.facebook_conversion_api_tls == false
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
else
http.use_ssl = true
end
request = Net::HTTP::Post.new(url.request_uri)
request['Content-Type'] = 'application/json'
request.body = payload.to_json
response = http.request(request)
if response.code.to_i >= 400
puts("CapiTransmitter Error: #{response.code} - #{response.body}")
else
puts("CapiTransmitter Success: #{event_name} sent for User #{user.id}")
end
rescue => e
puts("CapiTransmitter Exception: #{e.message}")
puts(e.backtrace.join("\n"))
end
end
end

View File

@ -4,20 +4,22 @@ module JamRuby
def self.send_reminders def self.send_reminders
begin begin
cutoff_date = Date.parse(Rails.application.config.profile_complete_reminders_effective_from_date) # Define a cutoff date for the profile completion emails
#If the user has not updated their profile 1 day after signup, then send reminder email1 #If the user has not updated their profile 1 day after signup, then send reminder email1
reminder1_users.find_each do |user| reminder1_users(cutoff_date).find_each do |user|
puts "reminder1_users user: #{user.id}"
UserMailer.profile_complete_reminder1(user).deliver_now UserMailer.profile_complete_reminder1(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder1_sent_at: Time.now) User.where(id: user.id).update_all(profile_complete_reminder1_sent_at: Time.now)
end end
#If the user has not updated their profile 3 days after signup, then send reminder email2 #If the user has not updated their profile 3 days after signup, then send reminder email2
reminder2_users.find_each do |user| reminder2_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder2(user).deliver_now UserMailer.profile_complete_reminder2(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder2_sent_at: Time.now) User.where(id: user.id).update_all(profile_complete_reminder2_sent_at: Time.now)
end end
#If the user has not updated their profile 5 days after signup, then send reminder email3 #If the user has not updated their profile 5 days after signup, then send reminder email3
reminder3_users.find_each do |user| reminder3_users(cutoff_date).find_each do |user|
UserMailer.profile_complete_reminder3(user).deliver_now UserMailer.profile_complete_reminder3(user).deliver_now
User.where(id: user.id).update_all(profile_complete_reminder3_sent_at: Time.now) User.where(id: user.id).update_all(profile_complete_reminder3_sent_at: Time.now)
end end
@ -31,18 +33,20 @@ module JamRuby
User.where("users.profile_completed_at IS NULL AND users.subscribe_email = ?", true) User.where("users.profile_completed_at IS NULL AND users.subscribe_email = ?", true)
end end
def self.reminder1_users def self.reminder1_users(cutoff_date)
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NULL", 1.day.ago) # ensure that the user has had the account for at least one day
puts "reminder1_users cutoff_date: #{cutoff_date}"
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end end
def self.reminder2_users def self.reminder2_users(cutoff_date)
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", 3.days.ago) # ensure that the user has had the account for at least three days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder1_sent_at < ? AND users.profile_complete_reminder1_sent_at IS NOT NULL AND users.profile_complete_reminder2_sent_at IS NULL", cutoff_date, 2.days.ago)
end end
def self.reminder3_users def self.reminder3_users(cutoff_date)
EmailProfileReminder.prospect_users.where("users.created_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", 5.days.ago) # ensure that user has had the profile for 5 days and guard against rapid back-to-back emails
EmailProfileReminder.prospect_users.where("users.created_at > ? AND users.profile_complete_reminder2_sent_at < ? AND users.profile_complete_reminder2_sent_at IS NOT NULL AND users.profile_complete_reminder3_sent_at IS NULL", cutoff_date, 2.days.ago)
end end
end end
end end

View File

@ -28,19 +28,19 @@ module JamRuby
end end
def self.prospect_users def self.prospect_users
User.where("users.first_certified_gear_at IS NULL") User.where("users.first_certified_gear_at IS NULL AND users.subscribe_email = ?", true)
end end
def self.reminder1_users(cutoff_date) def self.reminder1_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date) GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NULL AND (NOW() - users.created_at) > INTERVAL '1 day'", cutoff_date)
end end
def self.reminder2_users(cutoff_date) def self.reminder2_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date) GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder1_sent_at IS NOT NULL AND users.gear_setup_reminder2_sent_at IS NULL AND users.gear_setup_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end end
def self.reminder3_users(cutoff_date) def self.reminder3_users(cutoff_date)
GearSetupReminder.prospect_users.where("users.created_at < ? AND users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date) GearSetupReminder.prospect_users.where("users.created_at > ? AND users.gear_setup_reminder2_sent_at IS NOT NULL AND users.gear_setup_reminder3_sent_at IS NULL AND users.gear_setup_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end end
end end

View File

@ -36,11 +36,11 @@ module JamRuby
end end
def self.reminder2_users(cutoff_date) def self.reminder2_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder1_sent_at IS NOT NULL AND users.group_session_reminder2_sent_at IS NULL AND users.first_music_session_at < ?", cutoff_date, 3.days.ago) GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder1_sent_at IS NOT NULL AND users.group_session_reminder2_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder1_sent_at < ?", cutoff_date, 3.days.ago, 1.day.ago)
end end
def self.reminder3_users(cutoff_date) def self.reminder3_users(cutoff_date)
GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder2_sent_at IS NOT NULL AND users.group_session_reminder3_sent_at IS NULL AND users.first_music_session_at < ?", cutoff_date, 5.days.ago) GroupSessionReminder.prospect_users.where("users.created_at > ? AND users.group_session_reminder2_sent_at IS NOT NULL AND users.group_session_reminder3_sent_at IS NULL AND users.first_music_session_at < ? AND group_session_reminder2_sent_at < ?", cutoff_date, 5.days.ago, 1.day.ago)
end end
end end

View File

@ -27,19 +27,19 @@ module JamRuby
end end
def self.prospect_users def self.prospect_users
User.where("users.first_music_session_at IS NULL") User.where("users.first_music_session_at IS NULL AND first_certified_gear_at IS NOT NULL")
end end
def self.reminder1_users(cutoff_date) def self.reminder1_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NULL", 1.day.ago, cutoff_date) TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NULL AND (NOW() - users.first_certified_gear_at) > INTERVAL '1 day'", cutoff_date)
end end
def self.reminder2_users(cutoff_date) def self.reminder2_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at >= ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL", 3.days.ago, cutoff_date) TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder1_sent_at IS NOT NULL AND users.test_gear_reminder2_sent_at IS NULL AND users.test_gear_reminder1_sent_at < ?", cutoff_date, 2.days.ago)
end end
def self.reminder3_users(cutoff_date) def self.reminder3_users(cutoff_date)
TestGearReminder.prospect_users.where("users.first_certified_gear_at < ? AND users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL", 5.days.ago, cutoff_date) TestGearReminder.prospect_users.where("users.created_at > ? AND users.test_gear_reminder2_sent_at IS NOT NULL AND users.test_gear_reminder3_sent_at IS NULL AND users.test_gear_reminder2_sent_at < ?", cutoff_date, 2.days.ago)
end end
end end
end end

View File

@ -8,24 +8,26 @@ module JamRuby
class TrialExpiresReminder class TrialExpiresReminder
@@log = Logging.logger[TrialExpiresReminder] @@log = Logging.logger[TrialExpiresReminder]
FIRST_NOTIFICATION_CHECK = 1.day.ago.to_s FIRST_NOTIFICATION_CHECK = 1.day.ago
SECOND_NOTIFICATION_CHECK = 5.days.ago.to_s # unused here, but just as a form of simple documentation...
THIRD_NOTIFICATION_CHECK = 9.days.ago # SECOND_NOTIFICATION_CHECK = 4.days (from the previous reminder)
# THIRD_NOTIFICATION_CHECK = 4.days (from the previous reminder)
def self.prospect_users(cutoff_date) def self.prospect_users(cutoff_date)
User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.subscription_trial_ends_at > ?)", cutoff_date) User.where("(users.subscription_trial_ends_at IS NOT NULL AND users.created_at > ?)", cutoff_date)
end end
# trial_ended | in_trial
def self.reminder1_users(cutoff_date) def self.reminder1_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", 1.day.ago, FIRST_NOTIFICATION_CHECK) prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND users.subscription_trial_ends_at < ? AND users.trial_expires_reminder1_sent_at IS NULL", FIRST_NOTIFICATION_CHECK)
end end
def self.reminder2_users(cutoff_date) def self.reminder2_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL", 1.day.ago, SECOND_NOTIFICATION_CHECK) prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder1_sent_at IS NOT NULL AND users.trial_expires_reminder2_sent_at IS NULL AND (NOW() - trial_expires_reminder1_sent_at) > INTERVAL '4 days'")
end end
def self.reminder3_users(cutoff_date) def self.reminder3_users(cutoff_date)
prospect_users(cutoff_date).where("users.subscription_last_checked_at < ? AND users.subscription_trial_ends_at < ? AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL", 1.day.ago, THIRD_NOTIFICATION_CHECK) prospect_users(cutoff_date).where("users.subscription_sync_code = 'trial_ended' AND trial_expires_reminder2_sent_at IS NOT NULL AND users.trial_expires_reminder3_sent_at IS NULL AND (NOW() - trial_expires_reminder2_sent_at) > INTERVAL '4 days'")
end end
def self.send_reminders def self.send_reminders

View File

@ -1,8 +1,7 @@
module JamRuby module JamRuby
class Ars < ActiveRecord::Base class Ars < ActiveRecord::Base
attr_accessible :active, :name, :id_int, :ip, as: :admin attr_accessible :password, :username, :active, :beta, :name, :provider, :id_int, :ip, :port, :continent, :country, :city, :subdivision, :latitude, :longitude, as: :admin
self.table_name = "arses" self.table_name = "arses"
@@log = Logging.logger[Ars] @@log = Logging.logger[Ars]

View File

@ -76,7 +76,7 @@ module JamRuby
after_save :update_teacher_pct after_save :update_teacher_pct
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time, attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time,
:v2_photo_url, :v2_photo_uploaded :v2_photo_url, :v2_photo_uploaded, :facebook_click_id, :facebook_browser_id
# updating_password corresponds to a lost_password # updating_password corresponds to a lost_password
attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type
@ -542,6 +542,21 @@ module JamRuby
@updating_progression_field = true @updating_progression_field = true
if self[field_name].nil? if self[field_name].nil?
self[field_name] = time self[field_name] = time
# CAPI Hooks
begin
case field_name.to_s
when 'first_ran_client_at'
# StartTrial: When user opens the app
CapiTransmitter.send_event('StartTrial', self, { value: '0.00', currency: 'USD', predicted_ltv: '0.00' })
when 'first_certified_gear_at'
# AddToCart: When user finishes Gear Wizard
CapiTransmitter.send_event('AddToCart', self)
end
rescue => e
puts("Error sending #{field_name} CAPI event: #{e.message}")
end
self.save self.save
end end
end end
@ -1579,6 +1594,8 @@ module JamRuby
license_end = options[:license_end] license_end = options[:license_end]
import_source = options[:import_source] import_source = options[:import_source]
desired_plan_code = options[:desired_plan_code] desired_plan_code = options[:desired_plan_code]
facebook_click_id = options[:facebook_click_id]
facebook_browser_id = options[:facebook_browser_id]
if desired_plan_code == '' if desired_plan_code == ''
desired_plan_code = nil desired_plan_code = nil
@ -1640,16 +1657,24 @@ module JamRuby
musician = true musician = true
end end
user.musician = !!musician user.musician = !!musician
user.facebook_click_id = facebook_click_id
user.facebook_browser_id = facebook_browser_id
if origin if origin
user.origin_utm_source = origin["utm_source"] user.origin_utm_source = origin["utm_source"]
user.origin_utm_medium = origin["utm_medium"] user.origin_utm_medium = origin["utm_medium"]
user.origin_utm_campaign = origin["utm_campaign"] user.origin_utm_campaign = origin["utm_campaign"]
user.origin_id = origin["utm_id"]
user.origin_term = origin["utm_term"]
user.origin_content = origin["utm_content"]
user.origin_referrer = origin["referrer"] user.origin_referrer = origin["referrer"]
else else
user.origin_utm_source = 'organic' user.origin_utm_source = 'organic'
user.origin_utm_medium = 'organic' user.origin_utm_medium = 'organic'
user.origin_utm_campaign = nil user.origin_utm_campaign = nil
user.origin_id = nil
user.origin_term = nil
user.origin_content = nil
user.origin_referrer = nil user.origin_referrer = nil
end end
@ -1865,6 +1890,15 @@ module JamRuby
if user.errors.any? if user.errors.any?
raise ActiveRecord::Rollback raise ActiveRecord::Rollback
else else
# CAPI Hook: CompleteRegistration
if user.facebook_click_id.present?
begin
CapiTransmitter.send_event('CompleteRegistration', user)
rescue => e
puts("Error sending CompleteRegistration CAPI event: #{e.message}")
end
end
# if the partner ID was present and the partner doesn't already have a user associated, associate this new user with the affiliate partner # if the partner ID was present and the partner doesn't already have a user associated, associate this new user with the affiliate partner
if affiliate_partner && affiliate_partner.partner_user.nil? if affiliate_partner && affiliate_partner.partner_user.nil?
affiliate_partner.partner_user = user affiliate_partner.partner_user = user

View File

@ -336,6 +336,33 @@ module JamRuby
end end
end end
def report_meta_capi(current_user, plan_code)
# CAPI Hook: Subscribe
begin
monthly_cost = case plan_code
when JamRuby::SubscriptionDefinitions::JAM_SILVER, JamRuby::SubscriptionDefinitions::JAM_SILVER_YEARLY
5.00
when JamRuby::SubscriptionDefinitions::JAM_GOLD, JamRuby::SubscriptionDefinitions::JAM_GOLD_YEARLY
10.00
when JamRuby::SubscriptionDefinitions::JAM_PLATINUM, JamRuby::SubscriptionDefinitions::JAM_PLATINUM_YEARLY
20.00
else
0.00
end
ltv = monthly_cost * 12
begin
puts "Sending CAPI Subscribe event #{current_user.email}, #{monthly_cost}, #{ltv}"
CapiTransmitter.send_event('Subscribe', current_user, { value: monthly_cost.to_s, currency: 'USD', predicted_ltv: ltv.to_s })
rescue => e
puts "Error sending CAPI Subscribe event #{current_user.email}, #{e.message}"
end
rescue => e
puts "Error sending CAPI Subscribe event #{current_user.email}, #{e.message}"
end
end
def get_highest_plan(subscription) def get_highest_plan(subscription)
SubscriptionDefinitions.higher_plan(subscription.plan.plan_code, get_pending_plan_code(subscription)) SubscriptionDefinitions.higher_plan(subscription.plan.plan_code, get_pending_plan_code(subscription))
end end
@ -364,6 +391,8 @@ module JamRuby
end end
current_user.reset_playtime current_user.reset_playtime
current_user.save(validate: false) current_user.save(validate: false)
report_meta_capi(current_user, plan_code)
rescue => e rescue => e
puts "Could not create subscription for user #{current_user.email}. #{e}" puts "Could not create subscription for user #{current_user.email}. #{e}"
return false, subscription, account return false, subscription, account

View File

@ -19,13 +19,19 @@ describe TrialExpiresReminder do
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
end end
def no_more_emails_sent
UserMailer.deliveries.clear
TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(0)
end
it "sends reminder emails to users whose trials are about to expire" do it "sends reminder emails to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 1.days.from_now user1.subscription_trial_ends_at = 1.days.from_now
user1.subscription_last_checked_at = 2.days.ago user1.subscription_sync_code = 'trial_ended'
user1.save! user1.save!
user2.subscription_trial_ends_at = 1.days.ago user2.subscription_trial_ends_at = 2.days.ago
user2.subscription_last_checked_at = 2.days.ago user2.subscription_sync_code = 'trial_ended'
user2.save! user2.save!
TrialExpiresReminder.send_reminders TrialExpiresReminder.send_reminders
@ -36,11 +42,14 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.last.subject).to include("Your free gold trial has expired, but you have great options to keep playing!") expect(ActionMailer::Base.deliveries.last.subject).to include("Your free gold trial has expired, but you have great options to keep playing!")
expect(user2.reload.trial_expires_reminder1_sent_at).not_to be_nil expect(user2.reload.trial_expires_reminder1_sent_at).not_to be_nil
no_more_emails_sent
end end
it "does not send reminder emails to users who have already received them" do it "does not send reminder emails to users who have already received them" do
user1.subscription_trial_ends_at = 1.days.ago user1.reload
user1.subscription_last_checked_at = 2.days.ago user1.subscription_trial_ends_at = 1.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = Time.now user1.trial_expires_reminder1_sent_at = Time.now
user1.save! user1.save!
@ -50,31 +59,38 @@ describe TrialExpiresReminder do
end end
it "sends the second reminder email to users whose trials are about to expire" do it "sends the second reminder email to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 4.days.ago user1.reload
user1.subscription_last_checked_at = 1.days.ago user2.reload
user1.trial_expires_reminder1_sent_at = Time.now
# pretend that the first reminder email was sent 2 days ago
user1.subscription_trial_ends_at = 4.days.ago + 1.hour
user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder1_sent_at = 5.days.ago
user1.save! user1.save!
user2.subscription_trial_ends_at = 5.days.ago user2.subscription_trial_ends_at = 4.days.ago + 1.hour
user2.subscription_last_checked_at = 1.days.ago user2.subscription_sync_code = 'trial_ended'
user2.trial_expires_reminder1_sent_at = Time.now user2.trial_expires_reminder1_sent_at = Time.now
user2.save! user2.save!
TrialExpiresReminder.send_reminders TrialExpiresReminder.send_reminders
expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.count).to eq(1)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user2.email) expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email)
# Check if the second reminder email was sent by verifying the subject # Check if the second reminder email was sent by verifying the subject
expect(ActionMailer::Base.deliveries.last.subject).to include("Dont forget to check your options to keep playing") expect(ActionMailer::Base.deliveries.last.subject).to include("Dont forget to check your options to keep playing")
expect(user2.reload.trial_expires_reminder2_sent_at).not_to be_nil expect(user1.reload.trial_expires_reminder2_sent_at).not_to be_nil
no_more_emails_sent
end end
it "sends the third reminder email to users whose trials are about to expire" do it "sends the third reminder email to users whose trials are about to expire" do
user1.subscription_trial_ends_at = 10.days.ago user1.reload
user1.subscription_last_checked_at = 1.days.ago user1.subscription_trial_ends_at = 10.days.ago + 1.hour
user1.trial_expires_reminder1_sent_at = 6.days.ago user1.subscription_sync_code = 'trial_ended'
user1.trial_expires_reminder2_sent_at = 4.days.ago user1.trial_expires_reminder1_sent_at = 8.days.ago
user1.trial_expires_reminder2_sent_at = 9.days.ago
user1.save! user1.save!
TrialExpiresReminder.send_reminders TrialExpiresReminder.send_reminders
@ -85,22 +101,28 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.last.subject).to include("One last reminder!") expect(ActionMailer::Base.deliveries.last.subject).to include("One last reminder!")
expect(user1.reload.trial_expires_reminder3_sent_at).not_to be_nil expect(user1.reload.trial_expires_reminder3_sent_at).not_to be_nil
no_more_emails_sent
end end
it "sends first and second and third reminder emails to users whose trials are about to expire" do it "sends first and second and third reminder emails to users whose trials are about to expire" do
user1.reload
user2.reload
user3.reload
user1.subscription_trial_ends_at = 2.days.ago user1.subscription_trial_ends_at = 2.days.ago
user1.subscription_last_checked_at = 1.days.ago user1.subscription_sync_code = 'trial_ended'
user1.save! user1.save!
user2.subscription_trial_ends_at = 6.days.ago user2.subscription_trial_ends_at = 2.days.ago
user2.subscription_last_checked_at = 1.days.ago user2.trial_expires_reminder1_sent_at = 5.days.ago
user2.trial_expires_reminder1_sent_at = 4.days.ago user2.subscription_sync_code = 'trial_ended'
user2.save! user2.save!
user3.subscription_trial_ends_at = 10.days.ago user3.subscription_trial_ends_at = 2.days.ago
user3.subscription_last_checked_at = 2.days.ago user3.trial_expires_reminder1_sent_at = 8.days.ago
user3.trial_expires_reminder1_sent_at = 6.days.ago user3.trial_expires_reminder2_sent_at = 9.days.ago
user3.trial_expires_reminder2_sent_at = 4.days.ago user3.subscription_sync_code = 'trial_ended'
user3.save! user3.save!
TrialExpiresReminder.send_reminders TrialExpiresReminder.send_reminders
@ -112,5 +134,6 @@ describe TrialExpiresReminder do
expect(ActionMailer::Base.deliveries.count).to eq(3) expect(ActionMailer::Base.deliveries.count).to eq(3)
expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email, user2.email, user3.email) expect(ActionMailer::Base.deliveries.map(&:to).flatten).to include(user1.email, user2.email, user3.email)
no_more_emails_sent
end end
end end

View File

@ -55,6 +55,7 @@
//= require AAB_message_factory //= require AAB_message_factory
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require utils //= require utils
//= require subscription_utils //= require subscription_utils
//= require profile_utils //= require profile_utils

View File

@ -13,6 +13,7 @@
//= require AAB_message_factory //= require AAB_message_factory
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require layout //= require layout
//= require jamkazam //= require jamkazam
//= require utils //= require utils

View File

@ -8,5 +8,6 @@
//= require utils //= require utils
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require corp/init //= require corp/init
//= require_directory ../corp //= require_directory ../corp

View File

@ -64,6 +64,7 @@
//= require custom_controls //= require custom_controls
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require session_utils //= require session_utils
//= require recording_utils //= require recording_utils
//= require helpBubbleHelper //= require helpBubbleHelper

View File

@ -42,6 +42,7 @@
//= require ui_helper //= require ui_helper
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require recordingModel //= require recordingModel
//= require web/signup_helper //= require web/signup_helper
//= require web/signin_helper //= require web/signin_helper

View File

@ -0,0 +1,117 @@
/**
* meta_tracking.js
* A standalone module to capture and persist Meta attribution signals (fbclid, _fbp) in cookies.
*
* Logic adapted for legacy environment (no React hooks).
* - Checks URL for `fbclid` and sets `_fbc` cookie.
* - Checks for `_fbp` cookie; if missing, generates and sets it.
*/
(function (window, document) {
'use strict';
var MetaTracking = {
init: function () {
var location = window.location;
this.handleFbc(location.search);
this.handleFbp();
this.handleUtm(location.search);
},
// 1. Parsing and storing _fbc (Click ID)
handleFbc: function (searchParams) {
var fbclid = this.getQueryParam('fbclid', searchParams);
if (fbclid) {
var version = 'fb';
var subdomainIndex = 1; // 1 = example.com
var creationTime = new Date().getTime(); // Unix timestamp in ms
// Format: fb.1.timestamp.id
var fbcValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + fbclid;
this.setCookie('_fbc', fbcValue, 90);
}
},
handleUtm: function (searchParams) {
var self = this;
if (!searchParams) return;
// Logically, we want to capture all utm_ parameters.
// We can either iterate a list or dynamic regex.
// Given the requirement to be robust, let's look for "utm_"
var query = searchParams.substring(1); // remove '?'
var vars = query.split('&');
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split('=');
if (pair.length === 2) {
var key = decodeURIComponent(pair[0]);
var value = decodeURIComponent(pair[1]);
if (key.indexOf('utm_') === 0) {
self.setCookie(key, value, 90);
}
}
}
},
// 2. Handling _fbp (Browser ID)
handleFbp: function () {
if (!this.getCookie('_fbp')) {
var version = 'fb';
var subdomainIndex = 1;
var creationTime = new Date().getTime();
var randomInt = Math.floor(Math.random() * 10000000000); // 10-digit random number
// Format: fb.1.timestamp.randomDigits
var fbpValue = version + '.' + subdomainIndex + '.' + creationTime + '.' + randomInt;
this.setCookie('_fbp', fbpValue, 90);
}
},
// Helper: Get query param by name
getQueryParam: function (name, search) {
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
var results = regex.exec(search);
return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
},
// Helper: Set cookie
setCookie: function (name, value, days) {
var expires = "";
if (days) {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
// Ensure path is root and domain is included if needed (defaults to current host)
document.cookie = name + "=" + (value || "") + expires + "; path=/";
},
// Helper: Get cookie
getCookie: function (name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
};
// Initialize on ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
MetaTracking.init();
} else {
// IE9+ support for DOMContentLoaded
document.addEventListener('DOMContentLoaded', function () {
MetaTracking.init();
});
}
})(window, document);

View File

@ -23,6 +23,7 @@
//= require AAB_message_factory //= require AAB_message_factory
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require layout //= require layout
//= require jamkazam //= require jamkazam
//= require utils //= require utils
@ -37,11 +38,11 @@
function initializeInfluxDB() { function initializeInfluxDB() {
window.stats = new InfluxDB({ window.stats = new InfluxDB({
"host" : gon.global.influxdb_host, "host": gon.global.influxdb_host,
"port" : gon.global.influxdb_port, "port": gon.global.influxdb_port,
"username" : gon.global.influxdb_username, "username": gon.global.influxdb_username,
"password" : gon.global.influxdb_password, "password": gon.global.influxdb_password,
"database" : gon.global.influxdb_database "database": gon.global.influxdb_database
}); });
window.stats.write = window.stats.writePoint; window.stats.write = window.stats.writePoint;

View File

@ -60,6 +60,7 @@
//= require custom_controls //= require custom_controls
//= require jam_rest //= require jam_rest
//= require ga //= require ga
//= require meta_tracking
//= require session_utils //= require session_utils
//= require recording_utils //= require recording_utils
//= require helpBubbleHelper //= require helpBubbleHelper

View File

@ -1,7 +1,7 @@
class ApiJamTracksController < ApiController class ApiJamTracksController < ApiController
# have to be signed in currently to see this screen # have to be signed in currently to see this screen
before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index] before_filter :api_signed_in_user, :except => [:index, :autocomplete, :show_with_artist_info, :artist_index, :public_show]
before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index] before_filter :api_any_user, :only => [:index, :autocomplete, :show_with_artist_info, :artist_index]
before_filter :lookup_jam_track_right, :only => [:download, :enqueue, :show_jam_track_right, :mark_active, :download_stem] before_filter :lookup_jam_track_right, :only => [:download, :enqueue, :show_jam_track_right, :mark_active, :download_stem]
before_filter :ip_blacklist, :only => [:download_stem, :download] before_filter :ip_blacklist, :only => [:download_stem, :download]
@ -23,6 +23,11 @@ class ApiJamTracksController < ApiController
@jam_track = JamTrack.find_by_plan_code!(params[:plan_code]) @jam_track = JamTrack.find_by_plan_code!(params[:plan_code])
end end
def public_show
@jam_track = JamTrack.find_by!(slug: params[:slug])
render "api_jam_tracks/show_for_client", :layout => nil
end
def index def index
data = JamTrack.index(params, any_user) data = JamTrack.index(params, any_user)
@jam_tracks, @next, @count = data[0], data[1], data[2] @jam_tracks, @next, @count = data[0], data[1], data[2]

View File

@ -820,10 +820,6 @@ class ApiMusicSessionsController < ApiController
return render json: { code: "invalid_token", message: "No token found for '#{params[:token]}'" }, status: :forbidden return render json: { code: "invalid_token", message: "No token found for '#{params[:token]}'" }, status: :forbidden
end end
if token.expired_at < Time.now.utc
return render json: {code: "expired_token", message: "The token has expired" }, status: :forbidden
end
if token.token == Rails.application.config.video_open_room if token.token == Rails.application.config.video_open_room
return render json: { name: token.user.name, user_id: token.user.id }, status: 200 return render json: { name: token.user.name, user_id: token.user.id }, status: 200
end end

View File

@ -41,7 +41,9 @@ class ApiRecurlyController < ApiController
reuse_card: reuse_card_next_time, reuse_card: reuse_card_next_time,
affiliate_referral_id: cookies[:affiliate_visitor], affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie, origin: origin_cookie,
timezone: current_timezone timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
} }
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)

View File

@ -158,7 +158,9 @@ class ApiUsersController < ApiController
affiliate_referral_id: cookies[:affiliate_visitor], affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie, origin: origin_cookie,
test_drive_package: params[:test_drive_package], test_drive_package: params[:test_drive_package],
timezone: current_timezone timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
} }
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)

View File

@ -49,11 +49,22 @@ class ApplicationController < ActionController::Base
def origin_cookie def origin_cookie
begin begin
JSON.parse(cookies[:origin]) if cookies[:origin] data = JSON.parse(cookies[:origin]) if cookies[:origin]
rescue rescue
nil data = nil
end end
# Backfill with individual UTM cookies if present
# This supports cases where the frontend (jam-ui/web) set specific cookies
# or if the JSON cookie is missing/incomplete.
%w(utm_source utm_medium utm_campaign utm_term utm_content utm_id).each do |key|
if cookies[key].present?
data ||= {}
data[key] = cookies[key]
end
end
data
end end
def track_origin def track_origin

View File

@ -635,6 +635,10 @@ class LandingsController < ApplicationController
musician: true, musician: true,
timezone: current_timezone, timezone: current_timezone,
first_name: @first, first_name: @first,
origin: origin_cookie,
affiliate_referral_id: cookies[:affiliate_visitor],
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp],
instruments: [{:instrument_id => @instrument, :proficiency_level => 1, :priority => 1}]) instruments: [{:instrument_id => @instrument, :proficiency_level => 1, :priority => 1}])
if @user.errors.any? if @user.errors.any?
first = @user.errors.first first = @user.errors.first

View File

@ -120,7 +120,10 @@ class SessionsController < ApplicationController
last_name: auth_hash[:info][:last_name], last_name: auth_hash[:info][:last_name],
email: auth_hash[:info][:email], email: auth_hash[:info][:email],
timezone: current_timezone, timezone: current_timezone,
affiliate_referral_id: cookies[:affiliate_visitor]) affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp])
# Users who sign up using oauth are presumed to have valid email adddresses. # Users who sign up using oauth are presumed to have valid email adddresses.
user.confirm_email! user.confirm_email!
@ -196,7 +199,9 @@ class SessionsController < ApplicationController
location: {:country => nil, :state => nil, :city => nil}, location: {:country => nil, :state => nil, :city => nil},
affiliate_referral_id: cookies[:affiliate_visitor], affiliate_referral_id: cookies[:affiliate_visitor],
origin: origin_cookie, origin: origin_cookie,
timezone: current_timezone timezone: current_timezone,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp]
} }
options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options)

View File

@ -192,7 +192,11 @@ class UsersController < ApplicationController
affiliate_partner: @affiliate_partner, affiliate_partner: @affiliate_partner,
timezone: current_timezone, timezone: current_timezone,
origin: origin_cookie, origin: origin_cookie,
desired_plan_code: desired_plan_code ) timezone: current_timezone,
origin: origin_cookie,
desired_plan_code: desired_plan_code,
facebook_click_id: cookies[:_fbc],
facebook_browser_id: cookies[:_fbp])
rend = _render('new') rend = _render('new')
# check for errors # check for errors

View File

@ -10,10 +10,6 @@ node :jmep do |jam_track|
jam_track.jmep_json ? jam_track.jmep_json : nil jam_track.jmep_json ? jam_track.jmep_json : nil
end end
node :can_download do |item|
!!item.right_for_user(current_user, ShoppingCart::JAMTRACK_FULL)
end
child(:jam_track_tracks => :tracks) { child(:jam_track_tracks => :tracks) {
attributes :id, :part, :instrument, :track_type, :position attributes :id, :part, :instrument, :track_type, :position
@ -27,22 +23,28 @@ child(:jam_track_tracks => :tracks) {
end end
} }
node do |jam_track| if current_user
right = jam_track.right_for_user(current_user) node do |jam_track|
if right right = jam_track.right_for_user(current_user)
{ if right
jam_track_right_id: right.id, {
purchased_at: right.created_at.to_i, jam_track_right_id: right.id,
last_mixdown_id: right.last_mixdown_id, purchased_at: right.created_at.to_i,
last_stem_id: right.last_stem_id last_mixdown_id: right.last_mixdown_id,
} last_stem_id: right.last_stem_id
}
end
end end
end
node :mixdowns do |jam_track| node :mixdowns do |jam_track|
items = [] items = []
jam_track.mixdowns_for_user(current_user).each do |mixdown| jam_track.mixdowns_for_user(current_user).each do |mixdown|
items << partial("api_jam_track_mixdowns/show", :object => mixdown) items << partial("api_jam_track_mixdowns/show", :object => mixdown)
end
items
end
node :can_download do |item|
!!item.right_for_user(current_user, ShoppingCart::JAMTRACK_FULL)
end end
items
end end

View File

@ -1,3 +1,3 @@
object @jam_track object @jam_track
attributes :id, :name attributes :id, :name, :original_artist

View File

@ -173,6 +173,12 @@ if defined?(Bundler)
config.google_email = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8@developer.gserviceaccount.com' config.google_email = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8@developer.gserviceaccount.com'
config.google_public_server_key = "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" config.google_public_server_key = "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo"
# Facebook Conversions API
config.facebook_ad_source = Set.new(['facebook', 'instagram', 'meta', 'meta_ads', 'fb', 'ig', 'th', 'threads'])
config.facebook_pixel_id = "25663975269956141" # ONLY ONE. NO MULTI ENV
config.facebook_access_token = "EABAGQTtcGk4BQewKCztlfImT86ZAPsRAm3KDrYWsOFZAdLtJCa7bVOzdnZCnHZBFZBRMFVoSZCQdIpX01xXDM0ZCFmYrkPuAHdLsNBU61c7MZCZBLCFd3Q0qrxiJkeYr3F8FH3p98d0tGKljplhJ8vWBIKAiBXaUCCYk6urZB5qZAIEPsppWZCHOep91xZCtMVqtbGJQfFAZDZD" # ONLY ONE. NO MULTI ENV
config.facebook_conversion_api_tls = true
# Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more. # Use Private API Keys to communicate with Recurly's API v2. See https://docs.recurly.com/api/basics/authentication to learn more.
config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb' config.recurly_private_api_key = '55f2fdfa4d014e64a94eaba1e93f39bb'
# Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more. # Use Public Keys to identify your site when using Recurly.js. See https://docs.recurly.com/js/#include to learn more.
@ -523,6 +529,7 @@ if defined?(Bundler)
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL" config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10" config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10" config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24" config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12" config.group_session_reminders_effective_from_date = "2025-08-12"

View File

@ -128,6 +128,7 @@ SampleApp::Application.configure do
config.send_user_match_mail_only_to_jamkazam_team = false config.send_user_match_mail_only_to_jamkazam_team = false
config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL" config.signup_survey_url = "https://www.surveymonkey.com/r/WVBKLYL"
config.signup_survey_cutoff_date = "2025-06-10" config.signup_survey_cutoff_date = "2025-06-10"
config.profile_complete_reminders_effective_from_date = "2025-06-10"
config.gear_setup_reminders_effective_from_date = "2025-06-10" config.gear_setup_reminders_effective_from_date = "2025-06-10"
config.test_gear_reminders_effective_from_date = "2025-07-24" config.test_gear_reminders_effective_from_date = "2025-07-24"
config.group_session_reminders_effective_from_date = "2025-08-12" config.group_session_reminders_effective_from_date = "2025-08-12"

View File

@ -84,7 +84,7 @@ en:
regards: "Best Regards," regards: "Best Regards,"
signature: "JamKazam Team" signature: "JamKazam Team"
profile_complete_reminder2: profile_complete_reminder2:
subject: "Complete your JamKazam profile" subject: "Take 2 minutes to fill out your JamKazam profile now"
greeting: "Hello" greeting: "Hello"
paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to connect. Click the button below to update your profile now, before its shared with others!" paragraph1: "We share your profile with JamKazam members who may be a good fit to play music with you during your first week on the platform via an automated email feature. Dont miss this opportunity to connect. Click the button below to update your profile now, before its shared with others!"
update_profile: "Update Profile" update_profile: "Update Profile"
@ -92,7 +92,7 @@ en:
regards: "Best Regards," regards: "Best Regards,"
signature: "JamKazam Team" signature: "JamKazam Team"
profile_complete_reminder3: profile_complete_reminder3:
subject: "Complete your JamKazam profile" subject: "Last reminder to update your JamKazam profile"
greeting: "Hello" greeting: "Hello"
paragraph1: "Your profile is your key to connecting with other musicians on JamKazam. It lets others know what instruments you play at what level of proficiency and what kinds of music you like to play. This lets our existing community find and reach out to you to connect and play, and this information also powers our automated matchmaking features that make recommendations to you. If you think you might want to play music live online on JamKazam, please click the button below now to fill out your profile!" paragraph1: "Your profile is your key to connecting with other musicians on JamKazam. It lets others know what instruments you play at what level of proficiency and what kinds of music you like to play. This lets our existing community find and reach out to you to connect and play, and this information also powers our automated matchmaking features that make recommendations to you. If you think you might want to play music live online on JamKazam, please click the button below now to fill out your profile!"
update_profile: "Update Profile" update_profile: "Update Profile"

View File

@ -328,6 +328,7 @@ Rails.application.routes.draw do
match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased' match '/jamtracks/purchased' => 'api_jam_tracks#purchased', :via => :get, :as => 'api_jam_tracks_purchased'
match '/jamtracks/artists' => 'api_jam_tracks#artist_index', :via => :get, :as => 'api_jam_tracks_list_artists' match '/jamtracks/artists' => 'api_jam_tracks#artist_index', :via => :get, :as => 'api_jam_tracks_list_artists'
match '/jamtracks/:id' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show' match '/jamtracks/:id' => 'api_jam_tracks#show', :via => :get, :as => 'api_jam_tracks_show'
match '/jamtracks/public/:slug' => 'api_jam_tracks#public_show', :via => :get, :as => 'api_jam_tracks_public_show'
match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info' match '/jamtracks/band/:plan_code' => 'api_jam_tracks#show_with_artist_info', :via => :get, :as => 'api_jam_tracks_show_with_artist_info'
match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list' match '/jamtracks' => 'api_jam_tracks#index', :via => :get, :as => 'api_jam_tracks_list'
match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download' match '/jamtracks/download/:id' => 'api_jam_tracks#download', :via => :get, :as => 'api_jam_tracks_download'

File diff suppressed because it is too large Load Diff

View File

@ -466,5 +466,158 @@ namespace :jam_tracks do
end end
end end
task gen_jamtrack_manifest: :environment do |task, arg|
# DOWNLOAD_AUDIO =
max = ENV['MAX_JAMTRACKS'].to_i
if max == 0
max = nil
end
puts "MAX JAMTRACKS #{max}"
jam_tracks = JamTrack.includes([:jam_track_tracks,
{genres_jam_tracks: :genres},
{jam_track_tracks: :instrument},
:genres]).where(status: 'Production').order('original_artist, name')
private_bucket = Rails.application.config.aws_bucket
s3_manager = S3Manager.new(private_bucket, Rails.application.config.aws_access_key_id, Rails.application.config.aws_secret_access_key)
tmp_dir = Dir.mktmpdir
FileUtils.mkdir_p tmp_dir
puts "tmp_dir=#{tmp_dir}"
csv_file = File.join(tmp_dir, "manifest.csv")
top_folder = File.join(tmp_dir, "audio")
FileUtils.mkdir_p(top_folder)
CSV.open(csv_file, "wb") do |csv|
header = ['JamTrackId', 'TrackId', 'Artist', 'Song', 'Instrument', 'Part', 'Type', 'Genre', 'LocalOgg', 'LocalMeta', 's3_path_url_44', 's3_path_url_48', 'AudioExists']
csv << header
jam_tracks.each do |jam_track|
song = jam_track.name
jam_track.jam_track_tracks.each do |jam_track_track|
instrument = jam_track_track.instrument_id
part = jam_track_track.part ? jam_track_track.part : ''
# construct the meta file for this track:
meta = {}
meta[:jam_track_id] = jam_track.id
meta[:track_id] = jam_track_track.id
meta[:artist] = jam_track.original_artist
meta[:song] = jam_track.name
meta[:instrument] = instrument
meta[:part] = part
meta[:type] = jam_track_track.track_type
genre = jam_track.genres.first
meta[:genre] = genre ? genre.description : ''
meta[:s3_path_url_44] = "s3://#{private_bucket}/" + jam_track_track.url_44
meta[:s3_path_url_48] = "s3://#{private_bucket}/" + jam_track_track.url_48
meta_json = File.join(tmp_dir, "meta_#{jam_track.id}_#{jam_track_track.id}.json")
File.open(meta_json, "w") do |f|
f.write(JSON.pretty_generate(meta))
end
# find the first a-z, 0-9 character and use that
first_character = song[0].downcase
folder = File.join(top_folder, first_character)
if File.exist?(folder) == false
FileUtils.mkdir_p(folder)
end
# folder structure is:
# audio/a/song-instrument-part-type.ogg
# audio/a/song-instrument-part-type.ogg.meta
# run 'part' through a sanitizer to make it filesystem safe
part = part.gsub(/[^0-9A-Za-z]/, '_')
base_name = File.join(folder, "#{jam_track.id}_#{jam_track_track.id}_#{instrument}_#{part}_#{jam_track_track.track_type}".downcase)
ogg_file = "#{base_name}.ogg"
exists = s3_manager.exists?(jam_track_track.url_48)
row = []
row << jam_track.id
row << jam_track_track.id
row << jam_track.original_artist
row << jam_track.name
row << instrument
row << part
row << jam_track_track.track_type
genre = jam_track.genres.first
row << (genre ? genre.description : '')
row << ogg_file.gsub(tmp_dir, '')
row << ogg_file.gsub(tmp_dir, '') + ".meta"
row << "s3://#{private_bucket}/" + jam_track_track.url_44
row << "s3://#{private_bucket}/" + jam_track_track.url_48
row << exists
csv << row
FileUtils.cp(meta_json, "#{ogg_file}.meta")
# cleanup meta
FileUtils.rm(meta_json)
end
if max > 0
max -= 1
if max == 0
puts "Max of jamtracks reached"
break
end
end
end
end
# dump the 1st 10 lines to stdout for quick verification
File.open(csv_file, "r") do|f|
10.times do |i|
puts f.readline
end
end
s3_manager.upload('jam_track_manifests/manifest.csv', csv_file, content_type: 'text/csv')
folder_to_zip = top_folder
output_tar_path = Dir.mktmpdir
output_tar_file = "#{output_tar_path}/archive.tar.gz"
safe_output = Shellwords.escape(output_tar_file)
# -c = create
# -z = compress with gzip
# -f = to a file
# -P = (Optional, but useful) Preserve absolute paths.
# By default, 'tar' strips the leading '/' for security.
# A common pattern is to cd into the parent dir first.
# Safer way: cd to the directory to get relative paths
parent_dir = File.dirname(folder_to_zip)
folder_name = File.basename(folder_to_zip)
safe_parent = Shellwords.escape(parent_dir)
safe_folder_name = Shellwords.escape(folder_name)
# This command is safer as it creates the archive with relative paths
command = "tar -czf #{safe_output} -C #{safe_parent} #{safe_folder_name}"
success = system(command)
if success
puts "Successfully created tar.gz file."
else
puts "Failed to create tar.gz file."
end
s3_manager.upload('jam_track_manifests/all-tracks.tar.gz', safe_output, content_type: 'application/gzip')
puts "tar.gz output=#{safe_output}"
puts "tmp_dir=#{tmp_dir}"
end
end end

View File

@ -0,0 +1,27 @@
namespace :capi do
desc "Test Facebook CAPI connection"
task :test_connection => :environment do
puts "Testing CAPI connection..."
pixel_id = APP_CONFIG.facebook_pixel_id
access_token = APP_CONFIG.facebook_access_token
puts "Pixel ID: #{pixel_id}"
puts "Access Token: #{access_token.try(:truncate, 10)}"
if pixel_id.blank? || access_token.blank? || access_token == 'placeholder'
puts "WARNING: Configuration missing or placeholder."
end
# Mock user
user = User.last
user = User.find_by_email('seth@jamkazam.com')
if user
puts "Sending test event 'StartTrial' for User ID: #{user.id}"
#CapiTransmitter.send_event('StartTrial', user, { value: '0.00', currency: 'USD', predicted_ltv: '0.00' }, nil, ENV['TEST_EVENT_CODE'])
CapiTransmitter.send_event('AddToCart', user)
puts "Check Rails log for output."
else
puts "No user found in database."
end
end
end

View File

@ -52,6 +52,8 @@ class UserManager < BaseManager
license_end = options[:license_end] license_end = options[:license_end]
import_source = options[:import_source] import_source = options[:import_source]
desired_plan_code = options[:desired_plan_code] desired_plan_code = options[:desired_plan_code]
facebook_click_id = options[:facebook_click_id]
facebook_browser_id = options[:facebook_browser_id]
recaptcha_failed = false recaptcha_failed = false
unless options[:skip_recaptcha] # allow callers to opt-of recaptcha unless options[:skip_recaptcha] # allow callers to opt-of recaptcha
@ -114,7 +116,10 @@ class UserManager < BaseManager
license_start: license_start, license_start: license_start,
license_end: license_end, license_end: license_end,
import_source: import_source, import_source: import_source,
desired_plan_code: desired_plan_code) import_source: import_source,
desired_plan_code: desired_plan_code,
facebook_click_id: facebook_click_id,
facebook_browser_id: facebook_browser_id)
user user
end end

View File

@ -180,6 +180,22 @@ def web_config
def max_invites_to_receiver_per_day def max_invites_to_receiver_per_day
1000 # a low number only hinders test; the ruby rspec test have a lower value and test this feature 1000 # a low number only hinders test; the ruby rspec test have a lower value and test this feature
end end
def facebook_pixel_id
'1234567890'
end
def facebook_ad_source
'meta'
end
def facebook_access_token
'dummy_token'
end
def facebook_conversion_api_tls
true
end
end end
klass.new klass.new
end end