feat: add unique short ID suffix to visitor labels

Each visitor gets a deterministic 2-char suffix (letter+digit) from IP hash,
e.g. "Product Browser #K7". Distinguishes multiple visitors with same behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:41:31 +00:00
parent 64fd1f8330
commit 046dd67c1a

View File

@@ -68,25 +68,20 @@ class VisitorNickname
];
/**
* Generate a deterministic nickname from an IP address.
* Generate a short deterministic ID suffix from an IP address.
*
* Same IP always produces the same name. The hash is stable
* across PHP versions and platforms (crc32 + modulo).
* Same IP always produces the same suffix. Used to distinguish
* visitors with the same behavior label.
*
* @param string $ip IP address (v4 or v6)
* @return string e.g. "Vermillion Falcon"
* @return string e.g. "K7", "M3" — uppercase letter + digit
*/
public static function generate(string $ip): string
public static function shortId(string $ip): string
{
// Use crc32 for speed — we just need distribution, not security
$hash = crc32($ip);
// Make it unsigned
$hash = $hash & 0xFFFFFFFF;
$adjIdx = $hash % count(self::ADJECTIVES);
$nounIdx = (int) floor($hash / count(self::ADJECTIVES)) % count(self::NOUNS);
return ucfirst(self::ADJECTIVES[$adjIdx]) . ' ' . ucfirst(self::NOUNS[$nounIdx]);
$hash = crc32($ip) & 0xFFFFFFFF;
$letter = chr(65 + ($hash % 26)); // A-Z
$digit = ($hash >> 5) % 10; // 0-9
return $letter . $digit;
}
/**
@@ -207,12 +202,13 @@ class VisitorNickname
*
* Priority: order > cart > demo download > demo boot > returning visitor > visitor
*
* @param array $tags Output of behaviorTags()
* @param int $pages Total pages viewed
* @param int $sessions Number of sessions
* @return string Human-readable label, e.g. "Cart Abandoner"
* @param array $tags Output of behaviorTags()
* @param int $pages Total pages viewed
* @param int $sessions Number of sessions
* @param string $ip IP address (for unique suffix)
* @return string Human-readable label, e.g. "Cart Abandoner #K7"
*/
public static function primaryLabel(array $tags, int $pages = 0, int $sessions = 0): string
public static function primaryLabel(array $tags, int $pages = 0, int $sessions = 0, string $ip = ''): string
{
// Priority map — business funnel order, first match wins
$priority = [
@@ -231,18 +227,20 @@ class VisitorNickname
'first-visit' => 'New Visitor',
];
$suffix = $ip ? ' #' . self::shortId($ip) : '';
foreach ($priority as $tag => $label) {
if (in_array($tag, $tags)) {
return $label;
return $label . $suffix;
}
}
// Fallback
if ($pages > 1) {
return 'Visitor (' . $pages . 'p)';
return 'Visitor' . $suffix . ' (' . $pages . 'p)';
}
return 'Visitor';
return 'Visitor' . $suffix;
}
/**