Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/_funnel_discovery_worker.pl
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/_funnel_discovery_worker.pl

added on local at 2026-07-01 21:22:22

Added
+114
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to ba183c1112b8
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# AffSoft -- _funnel_discovery_worker.pl
4#
5# Auto-discovers top page sequences from anon_sessions + anon_pageviews
6# and materializes them as funnel rows with source='auto'. The admin can
7# rename, edit steps, or hide these in /admin_funnels.cgi. Hidden rows
8# stay hidden across worker re-runs (worker respects user state via
9# slug-idempotency).
10#
11# Runs hourly via cron.
12#======================================================================
13use strict;
14use warnings;
15use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
16use MODS::DBConnect;
17use MODS::AffSoft::Config;
18use Digest::MD5 qw(md5_hex);
19
20my $db = MODS::DBConnect->new;
21my $cfg = MODS::AffSoft::Config->new;
22my $DB = $cfg->settings('database_name');
23my $dbh = $db->db_connect;
24die "no dbh\n" unless $dbh;
25
26# Sequence separator (avoid arrows since page paths may contain -> characters)
27my $SEP = "\x1f";
28
29# Pull last 30 days of anon sessions, materialize their pageview sequences.
30my @rows = $db->db_readwrite_multiple($dbh, qq~
31 SELECT s.id,
32 GROUP_CONCAT(p.page_path ORDER BY p.sequence SEPARATOR '${SEP}') AS seq
33 FROM `${DB}`.anon_sessions s
34 JOIN `${DB}`.anon_pageviews p ON p.session_id = s.id
35 WHERE s.first_seen_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
36 GROUP BY s.id
37 HAVING seq IS NOT NULL AND seq != ''
38~, __FILE__, __LINE__);
39
40# Bucket by 2..4-step prefixes so we surface funnels of a few lengths.
41my %counts;
42foreach my $r (@rows) {
43 my @pages = split /$SEP/, ($r->{seq} // '');
44 next if scalar(@pages) < 2;
45
46 # Collapse consecutive duplicates (F5 spam).
47 my @clean;
48 foreach my $p (@pages) { push @clean, $p if !@clean || $clean[-1] ne $p; }
49 next if scalar(@clean) < 2;
50
51 my $max_end = $#clean < 3 ? $#clean : 3;
52 for my $end (1 .. $max_end) {
53 my @slice = @clean[0..$end];
54 my $key = join('|', @slice);
55 my $slug = 'auto_' . substr(md5_hex($key), 0, 16);
56 $counts{$slug} ||= { steps => \@slice, name => _name_from_slice(\@slice), n => 0 };
57 $counts{$slug}{n}++;
58 }
59}
60
61# Keep the top 8 sequences.
62my @top = sort { $counts{$b}{n} <=> $counts{$a}{n} } keys %counts;
63@top = @top[0..7] if scalar(@top) > 8;
64
65my $inserted = 0;
66my $skipped = 0;
67
68foreach my $slug (@top) {
69 my $entry = $counts{$slug};
70 next if $entry->{n} < 3; # need at least 3 sessions to bother
71
72 my $name = $entry->{name}; $name =~ s/'/''/g;
73 my $steps_j = _steps_json($entry->{steps}); $steps_j =~ s/'/''/g;
74 my $slug_q = $slug; $slug_q =~ s/'/''/g;
75
76 # Insert-or-noop: if the slug already exists, leave it alone so the
77 # user's rename / hide state is preserved.
78 my $r = $db->db_readwrite($dbh, qq~
79 SELECT id FROM `${DB}`.funnels WHERE slug='$slug_q' LIMIT 1
80 ~, __FILE__, __LINE__);
81 if ($r && $r->{id}) { $skipped++; next; }
82
83 $db->db_readwrite($dbh, qq~
84 INSERT INTO `${DB}`.funnels
85 (name, steps, is_active, is_hidden, source, slug, created_at)
86 VALUES
87 ('$name', '$steps_j', 1, 0, 'auto', '$slug_q', NOW())
88 ~, __FILE__, __LINE__);
89 $inserted++;
90}
91
92$db->db_disconnect($dbh);
93print "discovery: inserted=$inserted skipped=$skipped\n";
94
95sub _name_from_slice {
96 my ($slice) = @_;
97 return join(' -> ', map {
98 my $p = $_; $p =~ s{^/}{}; $p =~ s{\.cgi$}{}; $p = 'Home' if !length $p;
99 substr($p, 0, 40)
100 } @$slice);
101}
102
103sub _steps_json {
104 my ($slice) = @_;
105 my @parts;
106 foreach my $p (@$slice) {
107 my $label = $p; $label =~ s{^/}{}; $label = 'Home' if !length $label;
108 $label = substr($label, 0, 60);
109 my $p_esc = $p; $p_esc =~ s/\\/\\\\/g; $p_esc =~ s/"/\\"/g;
110 my $l_esc = $label; $l_esc =~ s/\\/\\\\/g; $l_esc =~ s/"/\\"/g;
111 push @parts, qq~{"kind":"path","match":"$p_esc","label":"$l_esc"}~;
112 }
113 return '[' . join(',', @parts) . ']';
114}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help