File size: 5,249 Bytes
c938f2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

declare(strict_types=1);

namespace Piwik\Plugins\BotTracking\tests\Unit;

use PHPUnit\Framework\TestCase;
use Piwik\DataTable;
use Piwik\DataTable\Row;
use Piwik\Plugins\BotTracking\Metrics;
use Piwik\Plugins\BotTracking\RecordBuilders\AIChatbotFavouredPages;

/**
 * Covers the pure union logic behind the Favoured Pages records (the merge that used to live in the
 * deleted FavouredPagesMerger). The query and archiving glue are exercised by the system ApiTest.
 *
 * @group BotTracking
 * @group AIChatbotFavouredPages
 * @group Plugins
 */
class AIChatbotFavouredPagesTest extends TestCase
{
    /**
     * @param array<int, array{string, int}> $rows list of [label, value]
     */
    private static function table(string $valueColumn, array $rows): DataTable
    {
        $table = new DataTable();
        foreach ($rows as [$label, $value]) {
            $table->addRow(new Row([Row::COLUMNS => ['label' => $label, $valueColumn => $value]]));
        }
        return $table;
    }

    /**
     * The normal (non-summary) rows as label => [human, ai].
     *
     * @return array<string, array{int, int}>
     */
    private static function asMap(DataTable $table): array
    {
        $out = [];
        foreach ($table->getRows() as $row) {
            if ($row->isSummaryRow()) {
                continue;
            }
            $out[(string) $row->getColumn('label')] = [
                (int) $row->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS),
                (int) $row->getColumn(Metrics::COLUMN_AI_CHATBOT_REQUESTS),
            ];
        }
        return $out;
    }

    public function testMergesOverlappingHumanOnlyAndAiOnlyUrls(): void
    {
        $human = self::table(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, [
            ['example.com/both', 100],
            ['example.com/human-only', 40],
        ]);
        $ai = self::table(Metrics::COLUMN_REQUESTS, [
            ['example.com/both', 7],
            ['example.com/ai-only', 12],
        ]);

        $merged = AIChatbotFavouredPages::mergeHumanAndAiTables($human, $ai);

        self::assertSame([
            'example.com/both'       => [100, 7],
            'example.com/ai-only'    => [0, 12],
            'example.com/human-only' => [40, 0],
        ], self::asMap($merged));
    }

    public function testMissingSidesDefaultToZero(): void
    {
        $merged = AIChatbotFavouredPages::mergeHumanAndAiTables(
            self::table(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, [['example.com/h', 5]]),
            self::table(Metrics::COLUMN_REQUESTS, [['example.com/a', 9]])
        );

        self::assertSame(['example.com/a' => [0, 9], 'example.com/h' => [5, 0]], self::asMap($merged));
    }

    public function testEmptyLabelsAreSkipped(): void
    {
        $merged = AIChatbotFavouredPages::mergeHumanAndAiTables(
            self::table(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, [['', 5], ['example.com/h', 3]]),
            self::table(Metrics::COLUMN_REQUESTS, [['', 9]])
        );

        self::assertSame(['example.com/h' => [3, 0]], self::asMap($merged));
    }

    public function testCombinesTruncationTailIntoUnscoredOthersRow(): void
    {
        $human = self::table(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, [['example.com/h', 3]]);
        $humanSummary = new Row([Row::COLUMNS => ['label' => 'Others', Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS => 999]]);
        $humanSummary->setIsSummaryRow();
        $human->addSummaryRow($humanSummary);

        $ai = self::table(Metrics::COLUMN_REQUESTS, [['example.com/a', 4]]);
        $aiSummary = new Row([Row::COLUMNS => ['label' => 'Others', Metrics::COLUMN_REQUESTS => 888]]);
        $aiSummary->setIsSummaryRow();
        $ai->addSummaryRow($aiSummary);

        $merged = AIChatbotFavouredPages::mergeHumanAndAiTables($human, $ai);

        // The normal per-URL union is unaffected by the truncation tails.
        self::assertSame(['example.com/a' => [0, 4], 'example.com/h' => [3, 0]], self::asMap($merged));

        // Both per-side "Others" tails combine into one summary row carrying both sums...
        $others = $merged->getSummaryRow();
        self::assertInstanceOf(Row::class, $others);
        self::assertSame(999, (int) $others->getColumn(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS));
        self::assertSame(888, (int) $others->getColumn(Metrics::COLUMN_AI_CHATBOT_REQUESTS));
        // ...and it is left unscored (no discrepancy score is computed for an aggregate of many URLs).
        self::assertFalse($others->getColumn(Metrics::COLUMN_DISCREPANCY_SCORE));
    }

    public function testProducesCanonicalColumnSet(): void
    {
        $merged = AIChatbotFavouredPages::mergeHumanAndAiTables(
            self::table(Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, [['example.com/h', 5]]),
            self::table(Metrics::COLUMN_REQUESTS, [['example.com/h', 2]])
        );

        $row = $merged->getFirstRow();
        self::assertSame(
            ['label', Metrics::COLUMN_UNIQUE_HUMAN_PAGEVIEWS, Metrics::COLUMN_AI_CHATBOT_REQUESTS],
            array_keys($row->getColumns())
        );
    }
}