Changeset 3743355
- Timestamp:
- 10/13/2026 09:15:13 AM (12 days ago)
- Location:
- redkirin-ad-click-verification
- Files:
-
- 4 added
- 26 edited
- 1 copied
-
tags/1.3.0 (copied) (copied from redkirin-ad-click-verification/trunk )
-
tags/1.3.0/assets/css/admin.css (modified) (1 diff)
-
tags/1.3.0/assets/js/rkacv.js (modified) (3 diffs)
-
tags/1.3.0/includes/class-rkacv-ad-visits.php (modified) (5 diffs)
-
tags/1.3.0/includes/class-rkacv-admin.php (modified) (15 diffs)
-
tags/1.3.0/includes/class-rkacv-alerts.php (added)
-
tags/1.3.0/includes/class-rkacv-dashboard.php (modified) (1 diff)
-
tags/1.3.0/includes/class-rkacv-forms.php (added)
-
tags/1.3.0/includes/class-rkacv-frontend.php (modified) (1 diff)
-
tags/1.3.0/includes/class-rkacv-log.php (modified) (2 diffs)
-
tags/1.3.0/includes/class-rkacv-settings.php (modified) (5 diffs)
-
tags/1.3.0/languages/redkirin-ad-click-verification-ja.mo (modified) ( previous)
-
tags/1.3.0/languages/redkirin-ad-click-verification-ja.po (modified) (2 diffs)
-
tags/1.3.0/readme.txt (modified) (3 diffs)
-
tags/1.3.0/redkirin-ad-click-verification.php (modified) (6 diffs)
-
tags/1.3.0/uninstall.php (modified) (1 diff)
-
trunk/assets/css/admin.css (modified) (1 diff)
-
trunk/assets/js/rkacv.js (modified) (3 diffs)
-
trunk/includes/class-rkacv-ad-visits.php (modified) (5 diffs)
-
trunk/includes/class-rkacv-admin.php (modified) (15 diffs)
-
trunk/includes/class-rkacv-alerts.php (added)
-
trunk/includes/class-rkacv-dashboard.php (modified) (1 diff)
-
trunk/includes/class-rkacv-forms.php (added)
-
trunk/includes/class-rkacv-frontend.php (modified) (1 diff)
-
trunk/includes/class-rkacv-log.php (modified) (2 diffs)
-
trunk/includes/class-rkacv-settings.php (modified) (5 diffs)
-
trunk/languages/redkirin-ad-click-verification-ja.mo (modified) ( previous)
-
trunk/languages/redkirin-ad-click-verification-ja.po (modified) (2 diffs)
-
trunk/readme.txt (modified) (3 diffs)
-
trunk/redkirin-ad-click-verification.php (modified) (6 diffs)
-
trunk/uninstall.php (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
redkirin-ad-click-verification/tags/1.3.0/assets/css/admin.css
r3743276 r3743355 78 78 tr.rkacv-dimmed th, tr.rkacv-dimmed td { opacity: .45; transition: opacity .15s; } 79 79 tr.rkacv-dimmed input, tr.rkacv-dimmed textarea, tr.rkacv-dimmed select { pointer-events: none; } 80 .rkacv-toolbar-right { display: flex; gap: 6px; align-items: center; } -
redkirin-ad-click-verification/tags/1.3.0/assets/js/rkacv.js
r3743325 r3743355 271 271 debug('human verified', r); 272 272 dlPush({ event: 'rkacv_human_verified', rkacv_human: true }); 273 if (cfg.verifiedPV) { 273 var thanks = onThanksPage(); 274 if (thanks && !recentFormSubmit()) { 275 // サンクスページに直接到達(フォーム送信を経ていない): 検証済みページビューを出さない。 276 debug('thanks page reached without a verified form submit'); 277 sendLog('blocked', 'thanks', String(w.location.pathname), r, 'no_form_submit'); 278 } else if (cfg.verifiedPV) { 279 if (thanks) { 280 dlPush({ event: 'rkacv_thanks_view', rkacv_page_path: String(w.location.pathname), rkacv_form_verified: true }); 281 sendLog('verified', 'thanks', String(w.location.pathname), r); 282 } 274 283 // page_view の代わりにキーイベントにできる「検証済みページビュー」。 275 284 dlPush({ … … 492 501 } 493 502 } catch (e) { /* noop */ } 503 } 504 505 /* ------------------------------------------------------------------ */ 506 /* フォーム送信の保護 */ 507 /* ------------------------------------------------------------------ */ 508 509 function formExcluded(form) { 510 var sels = cfg.formExclude || []; 511 for (var i = 0; i < sels.length; i++) { 512 try { 513 if (form.matches(sels[i]) || form.closest(sels[i])) { 514 return true; 515 } 516 } catch (e) { /* 不正なセレクタは無視 */ } 517 } 518 return false; 519 } 520 521 function setHidden(form, name, value) { 522 var el = form.querySelector('input[name="' + name + '"]'); 523 if (!el) { 524 el = d.createElement('input'); 525 el.type = 'hidden'; 526 el.name = name; 527 form.appendChild(el); 528 } 529 el.value = value; 530 } 531 532 function markFormVerified() { 533 try { 534 w.sessionStorage.setItem('rkacv_form_ok', JSON.stringify({ t: Date.now(), path: w.location.pathname })); 535 } catch (e) { /* noop */ } 536 } 537 538 function onSubmit(e) { 539 if (!cfg.forms) { 540 return; 541 } 542 var form = e.target; 543 if (!form || form.tagName !== 'FORM' || formExcluded(form)) { 544 return; 545 } 546 var r = evaluate(e, form); 547 var verdict = r.ok ? 'human' : (r.hard ? 'bot' : 'undetermined'); 548 var reasons = r.reasons.concat(r.notes || []).join(','); 549 550 // 判定結果を隠しフィールドで同送(CF7 / WPForms はサーバー側で bot を迷惑メール扱い、他は CRM 側で判別可能)。 551 setHidden(form, 'rkacv_verdict', verdict); 552 setHidden(form, 'rkacv_reasons', reasons); 553 setHidden(form, 'rkacv_dwell', String(r.dwell)); 554 setHidden(form, 'rkacv_signals', String(r.signals)); 555 setHidden(form, 'rkacv_token', cfg.logToken || ''); 556 557 var label = '<form' + (form.id ? '#' + form.id : '') + (form.getAttribute('action') ? ' ' + String(form.getAttribute('action')).slice(0, 80) : '') + '>'; 558 559 if (r.ok) { 560 state.lastVerifiedAt = now(); 561 markFormVerified(); 562 dlPush({ event: 'rkacv_form_submit', rkacv_form_id: form.id || '', rkacv_form_action: String(form.getAttribute('action') || '').slice(0, 200), rkacv_dwell_ms: r.dwell, rkacv_signals: r.signals }); 563 sendLog('verified', 'form', label, r); 564 return; 565 } 566 sendLog('blocked', 'form', label, r); 567 debug('form submit unverified', verdict, r.reasons); 568 569 if (r.hard && cfg.formBlockHard) { 570 // bot 兆候が明確なら送信そのものを止める(人間の可能性が残る場合は通す)。 571 e.preventDefault(); 572 e.stopImmediatePropagation(); 573 e.stopPropagation(); 574 } 575 } 576 577 /* ------------------------------------------------------------------ */ 578 /* サンクスページの到達検証 */ 579 /* ------------------------------------------------------------------ */ 580 581 function onThanksPage() { 582 var paths = cfg.thanksPaths || []; 583 var cur = w.location.pathname.replace(/\/+$/, '') + '/'; 584 for (var i = 0; i < paths.length; i++) { 585 var p = String(paths[i]).replace(/\/+$/, '') + '/'; 586 if (cur === p || cur.indexOf(p) === 0) { 587 return true; 588 } 589 } 590 return false; 591 } 592 593 function recentFormSubmit() { 594 try { 595 var raw = w.sessionStorage.getItem('rkacv_form_ok'); 596 if (!raw) { 597 return false; 598 } 599 var rec = JSON.parse(raw); 600 return rec && Date.now() - rec.t < (cfg.thanksWindow || 1800000); 601 } catch (e) { 602 return false; 603 } 494 604 } 495 605 … … 841 951 842 952 w.addEventListener('click', onClick, true); 953 w.addEventListener('submit', onSubmit, true); 843 954 checkIp(); 844 955 trackAdVisit(); -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-ad-visits.php
r3743244 r3743355 61 61 network VARCHAR(20) NOT NULL DEFAULT '', 62 62 source VARCHAR(40) NOT NULL DEFAULT '', 63 app VARCHAR(40) NOT NULL DEFAULT '', 63 64 extra VARCHAR(1000) NOT NULL DEFAULT '', 64 65 repeat_ip TINYINT UNSIGNED NOT NULL DEFAULT 0, … … 77 78 78 79 /** 80 * UA から流入アプリ(アプリ内ブラウザー)を判定。通常のブラウザーは空文字。 81 * 82 * @param string $ua UA。 83 * @return string 84 */ 85 public static function app_from_ua( $ua ) { 86 $ua = (string) $ua; 87 $map = array( 88 '/YJApp-(IOS|ANDROID)|jp\.co\.yahoo\./i' => 'Yahoo! JAPAN', 89 '/\bLine\/|\bLIFF\b/' => 'LINE', 90 '/FBAN|FBAV|FB_IAB/' => 'Facebook', 91 '/Instagram/' => 'Instagram', 92 '/TwitterAndroid|Twitter for iPhone/' => 'X', 93 '/\bGSA\//' => 'Google app', 94 '/SmartNews/i' => 'SmartNews', 95 '/Gunosy/i' => 'Gunosy', 96 '/NewsPicks/i' => 'NewsPicks', 97 '/TikTok|musical_ly|BytedanceWebview/i' => 'TikTok', 98 '/Pinterest/' => 'Pinterest', 99 '/MicroMessenger/' => 'WeChat', 100 '/Snapchat/' => 'Snapchat', 101 '/\bDuckDuckGo\//' => 'DuckDuckGo', 102 '/Mercari/i' => 'Mercari', 103 '/\bwv\)/' => 'Android WebView (other)', 104 ); 105 foreach ( $map as $re => $label ) { 106 if ( preg_match( $re, $ua ) ) { 107 return $label; 108 } 109 } 110 // iOS: Safari 本体には "Safari/" が付く。無い場合はアプリ内ブラウザー。 111 if ( false !== strpos( $ua, 'iPhone' ) || false !== strpos( $ua, 'iPad' ) ) { 112 if ( false === strpos( $ua, 'Safari/' ) && false === strpos( $ua, 'CriOS' ) && false === strpos( $ua, 'FxiOS' ) && false === strpos( $ua, 'EdgiOS' ) ) { 113 return 'iOS in-app (other)'; 114 } 115 } 116 return ''; 117 } 118 119 /** 79 120 * クエリ文字列の連想配列を列値に変換。 80 121 * … … 155 196 'network' => substr( sanitize_text_field( $row['network'] ), 0, 20 ), 156 197 'source' => substr( sanitize_text_field( $row['source'] ), 0, 40 ), 198 'app' => substr( self::app_from_ua( $row['user_agent'] ), 0, 40 ), 157 199 'extra' => substr( (string) $row['extra'], 0, 1000 ), 158 200 'repeat_ip' => $repeat_ip, … … 268 310 public static function breakdown( $column, $days, $limit = 20 ) { 269 311 global $wpdb; 270 if ( ! in_array( $column, array( 'campaign_id', 'placement', 'device', 'source', 'network', 'adgroup_id' ), true ) ) {312 if ( ! in_array( $column, array( 'campaign_id', 'placement', 'device', 'source', 'network', 'adgroup_id', 'app' ), true ) ) { 271 313 return array(); 272 314 } … … 368 410 369 411 /** 412 * 証跡エクスポート用: 期間内の bot(任意で保留も)着地。 413 * 414 * @param int $days 日数。 415 * @param bool $include_undetermined 保留も含める。 416 * @return array 417 */ 418 public static function evidence_rows( $days, $include_undetermined = false ) { 419 global $wpdb; 420 $table = self::table(); 421 $where = $include_undetermined ? "verdict IN ('bot','undetermined')" : "verdict = 'bot'"; 422 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 423 return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE created_at >= %s AND {$where} ORDER BY id ASC LIMIT 5000", self::since( $days ) ), ARRAY_A ); 424 } 425 426 /** 370 427 * URL から gclid 系の値を取り出す(クリックログとの突合用)。 371 428 * -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-admin.php
r3743325 r3743355 19 19 add_action( 'admin_post_rkacv_add_cidr', array( __CLASS__, 'handle_add_cidr' ) ); 20 20 add_action( 'admin_post_rkacv_clear_visits', array( __CLASS__, 'handle_clear_visits' ) ); 21 add_action( 'admin_post_rkacv_export', array( __CLASS__, 'handle_export' ) ); 21 22 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); 22 23 add_filter( 'plugin_action_links_' . plugin_basename( RKACV_PLUGIN_FILE ), array( __CLASS__, 'action_links' ) ); … … 76 77 RKACV_Ad_Visits::truncate(); 77 78 wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'tab' => 'ads', 'rkacv_msg' => 'visits_cleared' ), admin_url( 'options-general.php' ) ) ); 79 exit; 80 } 81 82 /** 83 * 証跡 CSV(無効クリックの申し立て用)。 84 */ 85 public static function handle_export() { 86 if ( ! current_user_can( 'manage_options' ) ) { 87 wp_die( esc_html__( 'You do not have permission to do this.', 'redkirin-ad-click-verification' ) ); 88 } 89 check_admin_referer( 'rkacv_export' ); 90 $days = isset( $_GET['days'] ) ? (int) $_GET['days'] : 7; 91 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 92 $days = 7; 93 } 94 $with_und = ! empty( $_GET['undetermined'] ); 95 96 nocache_headers(); 97 header( 'Content-Type: text/csv; charset=UTF-8' ); 98 header( 'Content-Disposition: attachment; filename="ad-click-verification-evidence-' . gmdate( 'Ymd' ) . '.csv"' ); 99 $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions 100 fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions 101 fputcsv( $out, array( 'datetime', 'type', 'verdict_or_reasons', 'click_id', 'campaign_id', 'placement', 'app', 'device', 'ip_anonymised', 'user_agent', 'page_url', 'dwell_ms', 'signals', 'link_url' ) ); 102 foreach ( RKACV_Ad_Visits::evidence_rows( $days, $with_und ) as $r ) { 103 fputcsv( $out, array( get_date_from_gmt( $r['created_at'], 'Y-m-d H:i:s' ), 'landing', $r['verdict'] . ( $r['reasons'] ? ' (' . $r['reasons'] . ')' : '' ), $r['gclid'], $r['campaign_id'], $r['placement'], $r['app'], $r['device'], $r['ip'], $r['user_agent'], $r['page_url'], $r['dwell_ms'], $r['signals'], '' ) ); 104 } 105 foreach ( RKACV_Log::evidence_rows( $days ) as $r ) { 106 fputcsv( $out, array( get_date_from_gmt( $r['created_at'], 'Y-m-d H:i:s' ), 'click', $r['reasons'], $r['click_id'], '', '', RKACV_Ad_Visits::app_from_ua( $r['user_agent'] ), '', $r['ip'], $r['user_agent'], $r['page_url'], $r['dwell_ms'], $r['signals'], $r['link_url'] ) ); 107 } 108 fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions 78 109 exit; 79 110 } … … 317 348 318 349 /** 基本設定タブの項目。 */ 319 const BASIC_KEYS = array( 'scope', 'verified_pageview', 'bot_tag_block', ' anonymize_ip', 'datalayer_event', 'debug', 'dedupe_daily_limit', 'dedupe_enabled', 'dedupe_window_min', 'dwell_signal_ms', 'enabled', 'gtag_direct', 'guard_datalayer', 'guarded_events', 'intercept_clicks', 'link_patterns', 'log_enabled', 'log_retention_days', 'log_verified', 'min_dwell_ms', 'min_signals' );350 const BASIC_KEYS = array( 'scope', 'verified_pageview', 'bot_tag_block', 'form_enabled', 'form_block_hard', 'form_exclude', 'thanks_paths', 'thanks_window_min', 'anonymize_ip', 'datalayer_event', 'debug', 'dedupe_daily_limit', 'dedupe_enabled', 'dedupe_window_min', 'dwell_signal_ms', 'enabled', 'gtag_direct', 'guard_datalayer', 'guarded_events', 'intercept_clicks', 'link_patterns', 'log_enabled', 'log_retention_days', 'log_verified', 'min_dwell_ms', 'min_signals' ); 320 351 321 352 /** 高度な設定タブの項目。 */ 322 const ADVANCED_KEYS = array( ' ad_capture_params', 'ad_report_delay_ms', 'ad_retention_days', 'ad_tracking', 'ad_trigger_params', 'bot_ua_patterns', 'deny_cidrs', 'deny_enabled', 'deny_ua', 'deny_ua_patterns', 'deny_webdriver', 'extra_cidrs', 'ip_check' );353 const ADVANCED_KEYS = array( 'cpc_default', 'cpc_by_campaign', 'currency_symbol', 'alert_enabled', 'alert_email', 'alert_bot_share', 'alert_min_landings', 'alert_repeat_clicks', 'alert_digest', 'ad_capture_params', 'ad_report_delay_ms', 'ad_retention_days', 'ad_tracking', 'ad_trigger_params', 'bot_ua_patterns', 'deny_cidrs', 'deny_enabled', 'deny_ua', 'deny_ua_patterns', 'deny_webdriver', 'extra_cidrs', 'ip_check' ); 323 354 324 355 private static function render_settings( $section = 'basic' ) { … … 347 378 self::field_number( $s, 'ad_report_delay_ms', __( 'Verdict delay (ms)', 'redkirin-ad-click-verification' ), __( 'How long to wait after the page starts loading before the landing is scored and sent. If the visitor leaves earlier, it is sent at that moment. Recommended 10000.', 'redkirin-ad-click-verification' ), 0, 120000, 'ad_tracking log_enabled' ); 348 379 self::field_number( $s, 'ad_retention_days', __( 'Ad landing retention (days)', 'redkirin-ad-click-verification' ), '', 1, 365, 'ad_tracking log_enabled' ); 380 ?> 381 </table> 382 383 <h2><?php esc_html_e( 'Wasted spend estimate', 'redkirin-ad-click-verification' ); ?></h2> 384 <table class="form-table" role="presentation"> 385 <?php 386 self::field_number( $s, 'cpc_default', __( 'Average CPC', 'redkirin-ad-click-verification' ), __( 'Average cost per click used for "bot landings x CPC" in the Ad traffic report and the dashboard. 0 hides the estimate.', 'redkirin-ad-click-verification' ), 0, 1000000 ); 387 self::field_textarea( $s, 'cpc_by_campaign', __( 'CPC per campaign', 'redkirin-ad-click-verification' ), __( 'One line per campaign as <code>campaign_id=cpc</code>, e.g. <code>21505995745=180</code>. Campaigns not listed use the average CPC.', 'redkirin-ad-click-verification' ), 4 ); 388 self::field_text( $s, 'currency_symbol', __( 'Currency symbol', 'redkirin-ad-click-verification' ) ); 389 ?> 390 </table> 391 392 <h2><?php esc_html_e( 'Alerts', 'redkirin-ad-click-verification' ); ?></h2> 393 <table class="form-table" role="presentation"> 394 <?php 395 self::field_checkbox( $s, 'alert_enabled', __( 'Send alert emails', 'redkirin-ad-click-verification' ), __( 'Checks the last 24 hours once an hour (WP-Cron) and emails when the bot share of ad landings or repeated conversions from one click ID exceed the thresholds. Each condition is reported at most once per 24 hours.', 'redkirin-ad-click-verification' ) ); 396 self::field_text( $s, 'alert_email', __( 'Recipient', 'redkirin-ad-click-verification' ), __( 'Leave empty to use the site admin email.', 'redkirin-ad-click-verification' ), 'alert_enabled' ); 397 self::field_number( $s, 'alert_bot_share', __( 'Bot share threshold (%)', 'redkirin-ad-click-verification' ), '', 1, 100, 'alert_enabled' ); 398 self::field_number( $s, 'alert_min_landings', __( 'Minimum landings for the share alert', 'redkirin-ad-click-verification' ), '', 1, 100000, 'alert_enabled' ); 399 self::field_number( $s, 'alert_repeat_clicks', __( 'Repeated conversions per click ID', 'redkirin-ad-click-verification' ), '', 2, 1000, 'alert_enabled' ); 400 self::field_checkbox( $s, 'alert_digest', __( 'Daily digest', 'redkirin-ad-click-verification' ), __( 'Once a day: blocked / verified clicks, ad landings and the wasted spend estimate.', 'redkirin-ad-click-verification' ), 'alert_enabled' ); 349 401 ?> 350 402 </table> … … 406 458 self::field_number( $s, 'dedupe_window_min', __( 'Window (minutes)', 'redkirin-ad-click-verification' ), __( 'Within this window, the same link type is measured only once per click ID / session. 0 disables the window rule.', 'redkirin-ad-click-verification' ), 0, 1440, 'dedupe_enabled' ); 407 459 self::field_number( $s, 'dedupe_daily_limit', __( 'Daily limit per click ID', 'redkirin-ad-click-verification' ), __( 'Maximum verified clicks per click ID in 24 hours, checked against the log via the REST API. 0 disables the limit.', 'redkirin-ad-click-verification' ), 0, 100, 'dedupe_enabled' ); 460 ?> 461 </table> 462 463 <h2><?php esc_html_e( 'Form submissions and thank-you pages', 'redkirin-ad-click-verification' ); ?></h2> 464 <table class="form-table" role="presentation"> 465 <?php 466 self::field_checkbox( $s, 'form_enabled', __( 'Verify form submissions', 'redkirin-ad-click-verification' ), __( 'Applies the same human verification to form submissions. Every submission gets hidden fields <code>rkacv_verdict</code> (human / undetermined / bot), <code>rkacv_reasons</code>, <code>rkacv_dwell</code> and <code>rkacv_signals</code>, so your CRM or form plugin can filter them. Verified submissions push <code>rkacv_form_submit</code> to the dataLayer; use it as the form key event.', 'redkirin-ad-click-verification' ) ); 467 self::field_checkbox( $s, 'form_block_hard', __( 'Block submissions with definite bot signals', 'redkirin-ad-click-verification' ), __( 'Stops the submission in the browser when webdriver, a bot user agent, automation globals or a synthetic submit event are present. Contact Form 7 marks such submissions as spam and WPForms rejects them on the server as well. Submissions that merely lack dwell time or interaction are never blocked.', 'redkirin-ad-click-verification' ), 'form_enabled' ); 468 self::field_textarea( $s, 'form_exclude', __( 'Forms to ignore', 'redkirin-ad-click-verification' ), __( 'CSS selectors, one per line. Matching forms (or forms inside a matching element) are not verified.', 'redkirin-ad-click-verification' ), 4, 'form_enabled' ); 469 self::field_textarea( $s, 'thanks_paths', __( 'Thank-you page paths', 'redkirin-ad-click-verification' ), __( 'One path per line, e.g. <code>/thanks/</code> or <code>/contact/complete/</code>. On these pages the verified page view event is pushed only if a verified form was submitted in this browser session within the window below, and <code>rkacv_thanks_view</code> is pushed as well. Direct visits and crawlers hitting the thank-you URL are logged and not counted. Leave empty to disable.', 'redkirin-ad-click-verification' ), 3, 'form_enabled' ); 470 self::field_number( $s, 'thanks_window_min', __( 'Thank-you window (minutes)', 'redkirin-ad-click-verification' ), '', 1, 1440, 'form_enabled' ); 408 471 ?> 409 472 </table> … … 626 689 'repeat_limit' => __( 'Repeat: daily limit per click ID', 'redkirin-ad-click-verification' ), 627 690 'key_on_touch' => __( 'Note: keyboard on touch device', 'redkirin-ad-click-verification' ), 691 'no_form_submit' => __( 'Thank-you page without a verified form submission', 'redkirin-ad-click-verification' ), 628 692 'deny_ua' => __( 'Denied: user agent', 'redkirin-ad-click-verification' ), 629 693 'deny_ip' => __( 'Denied: IP range', 'redkirin-ad-click-verification' ), … … 646 710 } 647 711 648 private static function breakdown_table( $title, $rows, $cv_by_key = array(), $key_label = '' ) {712 private static function breakdown_table( $title, $rows, $cv_by_key = array(), $key_label = '', $waste_by_key = null, $currency = '' ) { 649 713 if ( empty( $rows ) ) { 650 714 return; … … 662 726 <th><?php esc_html_e( 'Bot share', 'redkirin-ad-click-verification' ); ?></th> 663 727 <?php if ( $cv_by_key ) : ?><th class="rkacv-num"><?php esc_html_e( 'Verified clicks', 'redkirin-ad-click-verification' ); ?></th><?php endif; ?> 728 <?php if ( null !== $waste_by_key ) : ?><th class="rkacv-num"><?php esc_html_e( 'Est. waste', 'redkirin-ad-click-verification' ); ?></th><?php endif; ?> 664 729 </tr> 665 730 </thead> … … 680 745 </td> 681 746 <?php if ( $cv_by_key ) : ?><td class="rkacv-num"><?php echo esc_html( number_format_i18n( isset( $cv_by_key[ $r['key'] ] ) ? $cv_by_key[ $r['key'] ] : 0 ) ); ?></td><?php endif; ?> 747 <?php if ( null !== $waste_by_key ) : ?><td class="rkacv-num"><?php echo esc_html( $currency . number_format_i18n( isset( $waste_by_key[ $r['key'] ] ) ? $waste_by_key[ $r['key'] ] : 0 ) ); ?></td><?php endif; ?> 682 748 </tr> 683 749 <?php endforeach; ?> … … 705 771 $by_dev = RKACV_Ad_Visits::breakdown( 'device', $days ); 706 772 $by_src = RKACV_Ad_Visits::breakdown( 'source', $days ); 773 $by_app = RKACV_Ad_Visits::breakdown( 'app', $days ); 774 $cpc = RKACV_Settings::cpc_table( $s ); 775 $has_cpc = $cpc['default'] > 0 || ! empty( $cpc['by'] ); 776 $waste_by_cmp = array(); 777 $waste_total = 0; 778 if ( $has_cpc ) { 779 foreach ( $by_cmp as $c ) { 780 $rate = isset( $cpc['by'][ $c['key'] ] ) ? $cpc['by'][ $c['key'] ] : $cpc['default']; 781 $waste_by_cmp[ $c['key'] ] = $c['bot'] * $rate; 782 $waste_total += $c['bot'] * $rate; 783 } 784 } 785 $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=rkacv_export&days=' . $days ), 'rkacv_export' ); 707 786 $ips = RKACV_Ad_Visits::suspicious_ips( $days, 2 ); 708 787 $recent = RKACV_Ad_Visits::recent( 50 ); … … 726 805 <?php endforeach; ?> 727 806 </ul> 807 <div class="rkacv-toolbar-right"> 808 <a class="button" href="<?php echo esc_url( $export_url ); ?>"><?php esc_html_e( 'Export evidence CSV', 'redkirin-ad-click-verification' ); ?></a> 809 <a class="button" href="<?php echo esc_url( $export_url . '&undetermined=1' ); ?>" title="<?php esc_attr_e( 'Includes undetermined landings as well as bot signals', 'redkirin-ad-click-verification' ); ?>"><?php esc_html_e( 'Export incl. undetermined', 'redkirin-ad-click-verification' ); ?></a> 728 810 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" onsubmit="return confirm('<?php echo esc_js( __( 'Delete all ad landing records?', 'redkirin-ad-click-verification' ) ); ?>');"> 729 811 <input type="hidden" name="action" value="rkacv_clear_visits"> … … 731 813 <button class="button"><?php esc_html_e( 'Clear records', 'redkirin-ad-click-verification' ); ?></button> 732 814 </form> 815 </div> 733 816 </div> 817 <p class="description"><?php esc_html_e( 'The evidence CSV (bot landings and blocked clicks with time, click ID, campaign, app, user agent and reasons) can be attached to the Google Ads invalid click claim form.', 'redkirin-ad-click-verification' ); ?></p> 734 818 735 819 <div class="rkacv-cards"> 820 <?php if ( $has_cpc ) : ?> 821 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Estimated wasted spend', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value rkacv-blocked"><?php echo esc_html( $s['currency_symbol'] . number_format_i18n( $waste_total ) ); ?></div><div class="rkacv-card-sub"><?php esc_html_e( 'bot landings x CPC', 'redkirin-ad-click-verification' ); ?></div></div> 822 <?php endif; ?> 736 823 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Ad landings', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value"><?php echo esc_html( number_format_i18n( $sum['total'] ) ); ?></div></div> 737 824 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Bot signals', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value rkacv-blocked"><?php echo esc_html( number_format_i18n( $sum['bot'] ) ); ?> <small><?php echo esc_html( self::pct( $sum['bot'], $sum['total'] ) ); ?>%</small></div></div> … … 747 834 748 835 <?php 749 self::breakdown_table( __( 'By campaign', 'redkirin-ad-click-verification' ), $by_cmp, $cv_by_cmp, __( 'Campaign ID', 'redkirin-ad-click-verification' ) ); 836 self::breakdown_table( __( 'By campaign', 'redkirin-ad-click-verification' ), $by_cmp, $cv_by_cmp, __( 'Campaign ID', 'redkirin-ad-click-verification' ), $has_cpc ? $waste_by_cmp : null, $s['currency_symbol'] ); 837 self::breakdown_table( __( 'By app (in-app browser)', 'redkirin-ad-click-verification' ), $by_app, array(), __( 'App', 'redkirin-ad-click-verification' ) ); 750 838 self::breakdown_table( __( 'By placement', 'redkirin-ad-click-verification' ), $by_pl, array(), __( 'Placement', 'redkirin-ad-click-verification' ) ); 751 839 self::breakdown_table( __( 'By device', 'redkirin-ad-click-verification' ), $by_dev, array(), __( 'Device', 'redkirin-ad-click-verification' ) ); … … 806 894 <th><?php esc_html_e( 'Campaign ID', 'redkirin-ad-click-verification' ); ?></th> 807 895 <th><?php esc_html_e( 'Placement', 'redkirin-ad-click-verification' ); ?></th> 896 <th><?php esc_html_e( 'App', 'redkirin-ad-click-verification' ); ?></th> 808 897 <th><?php esc_html_e( 'Reason', 'redkirin-ad-click-verification' ); ?></th> 809 898 <th><?php esc_html_e( 'Dwell / signals', 'redkirin-ad-click-verification' ); ?></th> … … 828 917 <td class="rkacv-ellipsis"><?php echo esc_html( $r['campaign_id'] ); ?></td> 829 918 <td class="rkacv-ellipsis" title="<?php echo esc_attr( $r['placement'] ); ?>"><?php echo esc_html( $r['placement'] ); ?></td> 919 <td class="rkacv-ellipsis"><?php echo esc_html( isset( $r['app'] ) ? $r['app'] : '' ); ?></td> 830 920 <td><?php echo esc_html( self::reasons_label( $r['reasons'] ) ); ?></td> 831 921 <td class="rkacv-nowrap"><?php echo esc_html( $r['dwell_ms'] ); ?>ms / <?php echo esc_html( $r['signals'] ); ?></td> -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-dashboard.php
r3743244 r3743355 87 87 $bot_pct = round( $ads['bot'] / $ads['total'] * 100 ); 88 88 echo esc_html( sprintf( /* translators: 1: landings, 2: bot percentage, 3: undetermined count */ __( 'Ad landings: %1$d, %2$d%% with bot signals, %3$d undetermined', 'redkirin-ad-click-verification' ), $ads['total'], $bot_pct, $ads['undetermined'] ) ); 89 $cpc = RKACV_Settings::cpc_table( $settings ); 90 if ( $cpc['default'] > 0 || $cpc['by'] ) { 91 $waste = 0; 92 foreach ( RKACV_Ad_Visits::breakdown( 'campaign_id', self::DAYS, 100 ) as $c ) { 93 $waste += $c['bot'] * ( isset( $cpc['by'][ $c['key'] ] ) ? $cpc['by'][ $c['key'] ] : $cpc['default'] ); 94 } 95 echo ' · ' . esc_html( sprintf( /* translators: 1: currency, 2: amount */ __( 'est. wasted spend %1$s%2$s', 'redkirin-ad-click-verification' ), $settings['currency_symbol'], number_format_i18n( $waste ) ) ); 96 } 89 97 ?> 90 98 <a href="<?php echo esc_url( admin_url( 'options-general.php?page=' . RKACV_Admin::PAGE . '&tab=ads' ) ); ?>"><?php esc_html_e( 'Report', 'redkirin-ad-click-verification' ); ?></a> -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-frontend.php
r3743325 r3743355 50 50 'debug' => ! empty( $settings['debug'] ), 51 51 'denyWebdriver' => ! empty( $settings['deny_enabled'] ) && ! empty( $settings['deny_webdriver'] ), 52 'forms' => ! empty( $settings['form_enabled'] ), 53 'formBlockHard' => ! empty( $settings['form_block_hard'] ), 54 'formExclude' => RKACV_Settings::lines( $settings['form_exclude'] ), 55 'thanksPaths' => RKACV_Settings::lines( $settings['thanks_paths'] ), 56 'thanksWindow' => (int) $settings['thanks_window_min'] * 60 * 1000, 52 57 'verifiedPV' => ! empty( $settings['verified_pageview'] ), 53 58 'botTagBlock' => ! empty( $settings['bot_tag_block'] ), -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-log.php
r3743265 r3743355 10 10 class RKACV_Log { 11 11 12 const DB_VERSION = ' 4';12 const DB_VERSION = '5'; 13 13 14 14 /** 保存する最大行数。超えた分は古い行から削除する(リングバッファ)。 */ … … 103 103 } 104 104 return $ok; 105 } 106 107 /** 108 * 証跡エクスポート用: 期間内の遮断クリック。 109 * 110 * @param int $days 日数。 111 * @return array 112 */ 113 public static function evidence_rows( $days ) { 114 global $wpdb; 115 $table = self::table(); 116 $since = gmdate( 'Y-m-d H:i:s', time() - (int) $days * DAY_IN_SECONDS ); 117 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 118 return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE created_at >= %s AND status = 'blocked' ORDER BY id ASC LIMIT 5000", $since ), ARRAY_A ); 105 119 } 106 120 -
redkirin-ad-click-verification/tags/1.3.0/includes/class-rkacv-settings.php
r3743325 r3743355 104 104 'dedupe_window_min' => 30, 105 105 'dedupe_daily_limit' => 3, 106 // 無駄費用の推定(bot 着地 × CPC)。 107 'cpc_default' => 0, 108 'cpc_by_campaign' => '', 109 'currency_symbol' => '¥', 110 // フォーム送信の保護。 111 'form_enabled' => 1, 112 'form_block_hard' => 1, 113 'form_exclude' => "form[role=search]\n.search-form\n#loginform\n#adminbarsearch", 114 // サンクスページの到達検証(空なら無効)。 115 'thanks_paths' => '', 116 'thanks_window_min' => 30, 117 // 異常検知アラート。 118 'alert_enabled' => 0, 119 'alert_email' => '', 120 'alert_bot_share' => 30, 121 'alert_min_landings' => 20, 122 'alert_repeat_clicks' => 5, 123 'alert_digest' => 0, 106 124 // 広告着地の記録(アドフラウド可視化)。 107 125 'ad_tracking' => 1, … … 182 200 $out = array(); 183 201 184 foreach ( array( 'enabled', 'intercept_clicks', 'guard_datalayer', 'gtag_direct', 'ip_check', 'log_enabled', 'log_verified', 'anonymize_ip', 'debug', 'deny_enabled', 'deny_cidrs', 'deny_ua', 'deny_webdriver', 'ad_tracking', 'dedupe_enabled', 'verified_pageview', 'bot_tag_block' ) as $k ) {202 foreach ( array( 'enabled', 'intercept_clicks', 'guard_datalayer', 'gtag_direct', 'ip_check', 'log_enabled', 'log_verified', 'anonymize_ip', 'debug', 'deny_enabled', 'deny_cidrs', 'deny_ua', 'deny_webdriver', 'ad_tracking', 'dedupe_enabled', 'verified_pageview', 'bot_tag_block', 'form_enabled', 'form_block_hard', 'alert_enabled', 'alert_digest' ) as $k ) { 185 203 $out[ $k ] = empty( $input[ $k ] ) ? 0 : 1; 186 204 } … … 190 208 $out['dwell_signal_ms'] = isset( $input['dwell_signal_ms'] ) ? max( 0, min( 600000, (int) $input['dwell_signal_ms'] ) ) : $d['dwell_signal_ms']; 191 209 $out['log_retention_days'] = isset( $input['log_retention_days'] ) ? max( 1, min( 365, (int) $input['log_retention_days'] ) ) : $d['log_retention_days']; 210 $out['cpc_default'] = isset( $input['cpc_default'] ) ? max( 0, (float) $input['cpc_default'] ) : $d['cpc_default']; 211 $out['thanks_window_min'] = isset( $input['thanks_window_min'] ) ? max( 1, min( 1440, (int) $input['thanks_window_min'] ) ) : $d['thanks_window_min']; 212 $out['alert_bot_share'] = isset( $input['alert_bot_share'] ) ? max( 1, min( 100, (int) $input['alert_bot_share'] ) ) : $d['alert_bot_share']; 213 $out['alert_min_landings'] = isset( $input['alert_min_landings'] ) ? max( 1, min( 100000, (int) $input['alert_min_landings'] ) ) : $d['alert_min_landings']; 214 $out['alert_repeat_clicks'] = isset( $input['alert_repeat_clicks'] ) ? max( 2, min( 1000, (int) $input['alert_repeat_clicks'] ) ) : $d['alert_repeat_clicks']; 215 $out['currency_symbol'] = isset( $input['currency_symbol'] ) ? substr( sanitize_text_field( $input['currency_symbol'] ), 0, 5 ) : $d['currency_symbol']; 216 $out['alert_email'] = isset( $input['alert_email'] ) ? sanitize_text_field( $input['alert_email'] ) : $d['alert_email']; 192 217 $out['dedupe_window_min'] = isset( $input['dedupe_window_min'] ) ? max( 0, min( 1440, (int) $input['dedupe_window_min'] ) ) : $d['dedupe_window_min']; 193 218 $out['dedupe_daily_limit'] = isset( $input['dedupe_daily_limit'] ) ? max( 0, min( 100, (int) $input['dedupe_daily_limit'] ) ) : $d['dedupe_daily_limit']; … … 195 220 $out['ad_retention_days'] = isset( $input['ad_retention_days'] ) ? max( 1, min( 365, (int) $input['ad_retention_days'] ) ) : $d['ad_retention_days']; 196 221 197 foreach ( array( 'link_patterns', 'guarded_events', 'bot_ua_patterns', 'extra_cidrs', 'deny_ua_patterns', 'ad_trigger_params', 'ad_capture_params' ) as $k ) {222 foreach ( array( 'link_patterns', 'guarded_events', 'bot_ua_patterns', 'extra_cidrs', 'deny_ua_patterns', 'ad_trigger_params', 'ad_capture_params', 'cpc_by_campaign', 'form_exclude', 'thanks_paths' ) as $k ) { 198 223 $out[ $k ] = isset( $input[ $k ] ) ? self::sanitize_lines( $input[ $k ] ) : $d[ $k ]; 199 224 } … … 240 265 241 266 /** 267 * キャンペーン ID ごとの CPC("id=cpc" 行)とデフォルト CPC。 268 * 269 * @param array $settings 設定。 270 * @return array{default:float,by:array<string,float>} 271 */ 272 public static function cpc_table( $settings ) { 273 $by = array(); 274 foreach ( self::lines( $settings['cpc_by_campaign'] ) as $line ) { 275 if ( preg_match( '/^\s*([^=\s]+)\s*=\s*([0-9.]+)\s*$/', $line, $m ) ) { 276 $by[ $m[1] ] = (float) $m[2]; 277 } 278 } 279 return array( 280 'default' => (float) $settings['cpc_default'], 281 'by' => $by, 282 ); 283 } 284 285 /** 242 286 * 改行区切りテキストの整形。 243 287 * -
redkirin-ad-click-verification/tags/1.3.0/languages/redkirin-ad-click-verification-ja.po
r3743325 r3743355 2 2 msgid "" 3 3 msgstr "" 4 "Project-Id-Version: Redkirin Ad Click Verification 1. 2.0\n"4 "Project-Id-Version: Redkirin Ad Click Verification 1.3.0\n" 5 5 "POT-Creation-Date: 2026-10-12 00:08+0000\n" 6 6 "PO-Revision-Date: 2026-10-12 00:08+0000\n" … … 683 683 msgstr "別案として、page_view のキーイベントを残したまま GA4 設定タグに例外トリガー「rkacv_bot 等しい true」を追加する方法もあります。ただし読込時点で分かる bot にしか効きません。" 684 684 685 msgid "Wasted spend estimate" 686 msgstr "無駄費用の推定" 687 688 msgid "Average CPC" 689 msgstr "平均 CPC" 690 691 msgid "Average cost per click used for \"bot landings x CPC\" in the Ad traffic report and the dashboard. 0 hides the estimate." 692 msgstr "広告トラフィックのレポートとダッシュボードで「bot 着地 × CPC」の計算に使う平均クリック単価。0 で非表示。" 693 694 msgid "CPC per campaign" 695 msgstr "キャンペーン別 CPC" 696 697 msgid "One line per campaign as <code>campaign_id=cpc</code>, e.g. <code>21505995745=180</code>. Campaigns not listed use the average CPC." 698 msgstr "1 行 1 キャンペーンで <code>キャンペーンID=CPC</code> の形式(例: <code>21505995745=180</code>)。未記載のキャンペーンは平均 CPC を使います。" 699 700 msgid "Currency symbol" 701 msgstr "通貨記号" 702 703 msgid "Alerts" 704 msgstr "アラート" 705 706 msgid "Send alert emails" 707 msgstr "アラートメールを送る" 708 709 msgid "Checks the last 24 hours once an hour (WP-Cron) and emails when the bot share of ad landings or repeated conversions from one click ID exceed the thresholds. Each condition is reported at most once per 24 hours." 710 msgstr "1 時間ごと(WP-Cron)に直近 24 時間を確認し、広告着地の bot 率や同一クリック ID からの反復コンバージョンがしきい値を超えたらメールします。同じ条件の通知は 24 時間に 1 回までです。" 711 712 msgid "Recipient" 713 msgstr "宛先" 714 715 msgid "Leave empty to use the site admin email." 716 msgstr "空ならサイトの管理者メールアドレスを使います。" 717 718 msgid "Bot share threshold (%)" 719 msgstr "bot 率のしきい値 (%)" 720 721 msgid "Minimum landings for the share alert" 722 msgstr "bot 率アラートに必要な最低着地数" 723 724 msgid "Repeated conversions per click ID" 725 msgstr "クリック ID ごとの反復コンバージョン数" 726 727 msgid "Daily digest" 728 msgstr "日次ダイジェスト" 729 730 msgid "Once a day: blocked / verified clicks, ad landings and the wasted spend estimate." 731 msgstr "1 日 1 回、遮断 / 検証済みクリック、広告着地、無駄費用の推定を送ります。" 732 733 msgid "Form submissions and thank-you pages" 734 msgstr "フォーム送信とサンクスページ" 735 736 msgid "Verify form submissions" 737 msgstr "フォーム送信を検証する" 738 739 msgid "Applies the same human verification to form submissions. Every submission gets hidden fields <code>rkacv_verdict</code> (human / undetermined / bot), <code>rkacv_reasons</code>, <code>rkacv_dwell</code> and <code>rkacv_signals</code>, so your CRM or form plugin can filter them. Verified submissions push <code>rkacv_form_submit</code> to the dataLayer; use it as the form key event." 740 msgstr "フォーム送信にも同じ人間判定を適用します。すべての送信に隠しフィールド <code>rkacv_verdict</code>(human / undetermined / bot)、<code>rkacv_reasons</code>、<code>rkacv_dwell</code>、<code>rkacv_signals</code> が付くので、CRM やフォームプラグイン側で判別できます。検証済みの送信は <code>rkacv_form_submit</code> を dataLayer に push します。フォームのキーイベントにはこれを使ってください。" 741 742 msgid "Block submissions with definite bot signals" 743 msgstr "bot 兆候が明確な送信を止める" 744 745 msgid "Stops the submission in the browser when webdriver, a bot user agent, automation globals or a synthetic submit event are present. Contact Form 7 marks such submissions as spam and WPForms rejects them on the server as well. Submissions that merely lack dwell time or interaction are never blocked." 746 msgstr "webdriver・bot UA・自動化ツールの痕跡・合成 submit イベントがある場合、ブラウザー側で送信を止めます。Contact Form 7 では迷惑メール扱い、WPForms ではサーバー側でもエラーにします。滞在時間や操作が足りないだけの送信は止めません。" 747 748 msgid "Forms to ignore" 749 msgstr "対象外にするフォーム" 750 751 msgid "CSS selectors, one per line. Matching forms (or forms inside a matching element) are not verified." 752 msgstr "CSS セレクタを 1 行 1 件。一致するフォーム(または一致する要素内のフォーム)は検証しません。" 753 754 msgid "Thank-you page paths" 755 msgstr "サンクスページのパス" 756 757 msgid "One path per line, e.g. <code>/thanks/</code> or <code>/contact/complete/</code>. On these pages the verified page view event is pushed only if a verified form was submitted in this browser session within the window below, and <code>rkacv_thanks_view</code> is pushed as well. Direct visits and crawlers hitting the thank-you URL are logged and not counted. Leave empty to disable." 758 msgstr "1 行 1 パス(例: <code>/thanks/</code>、<code>/contact/complete/</code>)。これらのページでは、同じブラウザーセッションで下の時間内に検証済みのフォーム送信があった場合だけ検証済みページビューを push し、あわせて <code>rkacv_thanks_view</code> も push します。URL 直打ちやクローラーの到達はログに残し計測しません。空で無効。" 759 760 msgid "Thank-you window (minutes)" 761 msgstr "サンクスページの有効時間(分)" 762 763 msgid "Thank-you page without a verified form submission" 764 msgstr "検証済みフォーム送信のないサンクスページ到達" 765 766 msgid "Est. waste" 767 msgstr "推定無駄費用" 768 769 msgid "Export evidence CSV" 770 msgstr "証跡 CSV を出力" 771 772 msgid "Includes undetermined landings as well as bot signals" 773 msgstr "bot 兆候ありに加えて保留の着地も含めます" 774 775 msgid "Export incl. undetermined" 776 msgstr "保留を含めて出力" 777 778 msgid "The evidence CSV (bot landings and blocked clicks with time, click ID, campaign, app, user agent and reasons) can be attached to the Google Ads invalid click claim form." 779 msgstr "証跡 CSV(bot 着地と遮断クリック: 日時、クリック ID、キャンペーン、アプリ、UA、理由)は Google 広告の無効なクリックの申し立てフォームに添付できます。" 780 781 msgid "Estimated wasted spend" 782 msgstr "推定無駄費用" 783 784 msgid "bot landings x CPC" 785 msgstr "bot 着地 × CPC" 786 787 msgid "By app (in-app browser)" 788 msgstr "流入アプリ別(アプリ内ブラウザー)" 789 790 msgid "App" 791 msgstr "アプリ" 792 793 msgid "Bot share of ad landings in the last 24 hours: %1$d%% (%2$d landings, %3$d with bot signals)." 794 msgstr "直近 24 時間の広告着地の bot 率: %1$d%%(着地 %2$d 件、bot 兆候あり %3$d 件)。" 795 796 msgid "Campaign %1$s: %2$d bot / %3$d landings" 797 msgstr "キャンペーン %1$s: bot %2$d / 着地 %3$d" 798 799 msgid "Repeated conversions from one ad click ID %1$s: %2$d verified, %3$d repeat-blocked, %4$d IPs in the last 24 hours." 800 msgstr "1 つの広告クリック ID %1$s から反復コンバージョン: 直近 24 時間で検証済み %2$d 件、反復遮断 %3$d 件、IP 数 %4$d。" 801 802 msgid "Ad Click Verification alert" 803 msgstr "Ad Click Verification アラート" 804 805 msgid "Clicks in the last 24 hours: %1$d blocked (%2$d with bot signals), %3$d verified, %4$d page requests denied." 806 msgstr "直近 24 時間のクリック: 遮断 %1$d 件(bot 兆候あり %2$d 件)、検証済み %3$d 件、表示拒否 %4$d 件。" 807 808 msgid "Ad landings: %1$d (%2$d bot signals, %3$d undetermined, %4$d human)." 809 msgstr "広告着地: %1$d 件(bot 兆候あり %2$d、保留 %3$d、人間 %4$d)。" 810 811 msgid "Estimated wasted spend (bot landings x CPC): %1$s%2$s" 812 msgstr "推定無駄費用(bot 着地 × CPC): %1$s%2$s" 813 814 msgid "Ad Click Verification daily digest" 815 msgstr "Ad Click Verification 日次ダイジェスト" 816 817 msgid "est. wasted spend %1$s%2$s" 818 msgstr "推定無駄費用 %1$s%2$s" 819 820 msgid "The submission could not be verified. Please try again." 821 msgstr "送信を確認できませんでした。もう一度お試しください。" 822 -
redkirin-ad-click-verification/tags/1.3.0/readme.txt
r3743325 r3743355 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 * **Click interception** – unverified clicks are stopped at the `window` capture phase, before Google Tag Manager click triggers or inline `onclick` handlers see them. The navigation itself (dialing, opening LINE, following the link) is performed by the plugin, so visitors are never blocked. 22 22 * **dataLayer guard** – `gtm.linkClick` / `gtm.click` and any event names you specify are dropped from `dataLayer.push` while the visitor is unverified. 23 * **Form submission verification** – the same human verification applies to form submissions. Hidden fields carry the verdict for your CRM, verified submissions push <code>rkacv_form_submit</code>, submissions with definite bot signals are stopped (Contact Form 7 marks them as spam, WPForms rejects them), and thank-you pages only count when a verified form was submitted in the same session. 24 * **In-app browser detection** – landings are classified by app (Yahoo! JAPAN, LINE, Facebook, Instagram, X, Google app, TikTok, generic WebView, ...), so you can see which apps bot traffic comes from even without ValueTrack placements. 25 * **Wasted spend estimate** – enter an average CPC (or per campaign) and the report shows bot landings x CPC per campaign and in total. 26 * **Evidence export** – CSV of bot landings and blocked clicks for the Google Ads invalid click claim form. 27 * **Alerts** – hourly check with email when the bot share of ad landings or repeated conversions from one click ID exceed your thresholds; optional daily digest. 23 28 * **Page view protection** – obvious bots (webdriver, bot user agents, automation globals) get no tags at all: every dataLayer push is dropped, so GTM and gtag.js send nothing, not even page_view. Once a visitor is verified, a <code>rkacv_verified_pageview</code> event is pushed that you can use as a key event instead of page_view (thank-you pages, landing pages). 24 29 * **Verified events** – verified clicks push `rkacv_verified_click` to the dataLayer (with `rkacv_link_type`, `rkacv_link_url`, `rkacv_link_text`, `rkacv_element_id`), and can optionally call `gtag('event', ...)` directly. … … 94 99 == Changelog == 95 100 101 = 1.3.0 = 102 * In-app browser classification for ad landings (new "By app" breakdown and column); evidence CSV export for Google Ads invalid click claims; wasted spend estimate (bot landings x CPC, per campaign CPC overrides) in the report, the dashboard and the digest. 103 * Form submission verification: hidden verdict fields, rkacv_form_submit event for verified submissions, blocking of submissions with definite bot signals (browser, Contact Form 7 spam, WPForms error), configurable excluded forms. 104 * Thank-you page gating: verified page view / rkacv_thanks_view only after a verified form submission in the same session; direct hits are logged. 105 * Alert emails (hourly check: bot share, repeated conversions per click ID) and an optional daily digest. 106 96 107 = 1.2.0 = 97 108 * New "Fire no tags for obvious bots" (default on): when webdriver, a bot user agent or automation globals are present at load, every dataLayer push is dropped so GTM / gtag.js send nothing, including page_view. -
redkirin-ad-click-verification/tags/1.3.0/redkirin-ad-click-verification.php
r3743325 r3743355 3 3 * Plugin Name: Redkirin Ad Click Verification 4 4 * Description: Stops bots from inflating GA4 events and Google Ads conversions by verifying that clicks on phone, LINE, email and other tracked links come from a real person. 5 * Version: 1. 2.05 * Version: 1.3.0 6 6 * Author: Redkirin 7 7 * Author URI: https://redkirin.co.jp/ … … 17 17 } 18 18 19 define( 'RKACV_VERSION', '1. 2.0' );19 define( 'RKACV_VERSION', '1.3.0' ); 20 20 define( 'RKACV_PLUGIN_FILE', __FILE__ ); 21 21 define( 'RKACV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 30 30 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-frontend.php'; 31 31 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-deny.php'; 32 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-forms.php'; 33 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-alerts.php'; 32 34 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-admin.php'; 33 35 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-dashboard.php'; … … 43 45 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'rkacv_daily_maintenance' ); 44 46 } 47 RKACV_Alerts::schedule(); 45 48 46 49 $settings = RKACV_Settings::get(); … … 56 59 function rkacv_deactivate() { 57 60 wp_clear_scheduled_hook( 'rkacv_daily_maintenance' ); 61 RKACV_Alerts::unschedule(); 58 62 } 59 63 register_deactivation_hook( __FILE__, 'rkacv_deactivate' ); … … 90 94 RKACV_Frontend::init(); 91 95 RKACV_Deny::init(); 96 RKACV_Forms::init(); 97 RKACV_Alerts::init(); 98 RKACV_Alerts::schedule(); 92 99 if ( is_admin() ) { 93 100 RKACV_Admin::init(); -
redkirin-ad-click-verification/tags/1.3.0/uninstall.php
r3743244 r3743355 27 27 delete_transient( 'rkacv_ip_refresh_lock' ); 28 28 delete_transient( 'rkacv_rl_global' ); 29 wp_clear_scheduled_hook( 'rkacv_hourly_check' ); 30 delete_transient( 'rkacv_alert_share' ); -
redkirin-ad-click-verification/trunk/assets/css/admin.css
r3743276 r3743355 78 78 tr.rkacv-dimmed th, tr.rkacv-dimmed td { opacity: .45; transition: opacity .15s; } 79 79 tr.rkacv-dimmed input, tr.rkacv-dimmed textarea, tr.rkacv-dimmed select { pointer-events: none; } 80 .rkacv-toolbar-right { display: flex; gap: 6px; align-items: center; } -
redkirin-ad-click-verification/trunk/assets/js/rkacv.js
r3743325 r3743355 271 271 debug('human verified', r); 272 272 dlPush({ event: 'rkacv_human_verified', rkacv_human: true }); 273 if (cfg.verifiedPV) { 273 var thanks = onThanksPage(); 274 if (thanks && !recentFormSubmit()) { 275 // サンクスページに直接到達(フォーム送信を経ていない): 検証済みページビューを出さない。 276 debug('thanks page reached without a verified form submit'); 277 sendLog('blocked', 'thanks', String(w.location.pathname), r, 'no_form_submit'); 278 } else if (cfg.verifiedPV) { 279 if (thanks) { 280 dlPush({ event: 'rkacv_thanks_view', rkacv_page_path: String(w.location.pathname), rkacv_form_verified: true }); 281 sendLog('verified', 'thanks', String(w.location.pathname), r); 282 } 274 283 // page_view の代わりにキーイベントにできる「検証済みページビュー」。 275 284 dlPush({ … … 492 501 } 493 502 } catch (e) { /* noop */ } 503 } 504 505 /* ------------------------------------------------------------------ */ 506 /* フォーム送信の保護 */ 507 /* ------------------------------------------------------------------ */ 508 509 function formExcluded(form) { 510 var sels = cfg.formExclude || []; 511 for (var i = 0; i < sels.length; i++) { 512 try { 513 if (form.matches(sels[i]) || form.closest(sels[i])) { 514 return true; 515 } 516 } catch (e) { /* 不正なセレクタは無視 */ } 517 } 518 return false; 519 } 520 521 function setHidden(form, name, value) { 522 var el = form.querySelector('input[name="' + name + '"]'); 523 if (!el) { 524 el = d.createElement('input'); 525 el.type = 'hidden'; 526 el.name = name; 527 form.appendChild(el); 528 } 529 el.value = value; 530 } 531 532 function markFormVerified() { 533 try { 534 w.sessionStorage.setItem('rkacv_form_ok', JSON.stringify({ t: Date.now(), path: w.location.pathname })); 535 } catch (e) { /* noop */ } 536 } 537 538 function onSubmit(e) { 539 if (!cfg.forms) { 540 return; 541 } 542 var form = e.target; 543 if (!form || form.tagName !== 'FORM' || formExcluded(form)) { 544 return; 545 } 546 var r = evaluate(e, form); 547 var verdict = r.ok ? 'human' : (r.hard ? 'bot' : 'undetermined'); 548 var reasons = r.reasons.concat(r.notes || []).join(','); 549 550 // 判定結果を隠しフィールドで同送(CF7 / WPForms はサーバー側で bot を迷惑メール扱い、他は CRM 側で判別可能)。 551 setHidden(form, 'rkacv_verdict', verdict); 552 setHidden(form, 'rkacv_reasons', reasons); 553 setHidden(form, 'rkacv_dwell', String(r.dwell)); 554 setHidden(form, 'rkacv_signals', String(r.signals)); 555 setHidden(form, 'rkacv_token', cfg.logToken || ''); 556 557 var label = '<form' + (form.id ? '#' + form.id : '') + (form.getAttribute('action') ? ' ' + String(form.getAttribute('action')).slice(0, 80) : '') + '>'; 558 559 if (r.ok) { 560 state.lastVerifiedAt = now(); 561 markFormVerified(); 562 dlPush({ event: 'rkacv_form_submit', rkacv_form_id: form.id || '', rkacv_form_action: String(form.getAttribute('action') || '').slice(0, 200), rkacv_dwell_ms: r.dwell, rkacv_signals: r.signals }); 563 sendLog('verified', 'form', label, r); 564 return; 565 } 566 sendLog('blocked', 'form', label, r); 567 debug('form submit unverified', verdict, r.reasons); 568 569 if (r.hard && cfg.formBlockHard) { 570 // bot 兆候が明確なら送信そのものを止める(人間の可能性が残る場合は通す)。 571 e.preventDefault(); 572 e.stopImmediatePropagation(); 573 e.stopPropagation(); 574 } 575 } 576 577 /* ------------------------------------------------------------------ */ 578 /* サンクスページの到達検証 */ 579 /* ------------------------------------------------------------------ */ 580 581 function onThanksPage() { 582 var paths = cfg.thanksPaths || []; 583 var cur = w.location.pathname.replace(/\/+$/, '') + '/'; 584 for (var i = 0; i < paths.length; i++) { 585 var p = String(paths[i]).replace(/\/+$/, '') + '/'; 586 if (cur === p || cur.indexOf(p) === 0) { 587 return true; 588 } 589 } 590 return false; 591 } 592 593 function recentFormSubmit() { 594 try { 595 var raw = w.sessionStorage.getItem('rkacv_form_ok'); 596 if (!raw) { 597 return false; 598 } 599 var rec = JSON.parse(raw); 600 return rec && Date.now() - rec.t < (cfg.thanksWindow || 1800000); 601 } catch (e) { 602 return false; 603 } 494 604 } 495 605 … … 841 951 842 952 w.addEventListener('click', onClick, true); 953 w.addEventListener('submit', onSubmit, true); 843 954 checkIp(); 844 955 trackAdVisit(); -
redkirin-ad-click-verification/trunk/includes/class-rkacv-ad-visits.php
r3743244 r3743355 61 61 network VARCHAR(20) NOT NULL DEFAULT '', 62 62 source VARCHAR(40) NOT NULL DEFAULT '', 63 app VARCHAR(40) NOT NULL DEFAULT '', 63 64 extra VARCHAR(1000) NOT NULL DEFAULT '', 64 65 repeat_ip TINYINT UNSIGNED NOT NULL DEFAULT 0, … … 77 78 78 79 /** 80 * UA から流入アプリ(アプリ内ブラウザー)を判定。通常のブラウザーは空文字。 81 * 82 * @param string $ua UA。 83 * @return string 84 */ 85 public static function app_from_ua( $ua ) { 86 $ua = (string) $ua; 87 $map = array( 88 '/YJApp-(IOS|ANDROID)|jp\.co\.yahoo\./i' => 'Yahoo! JAPAN', 89 '/\bLine\/|\bLIFF\b/' => 'LINE', 90 '/FBAN|FBAV|FB_IAB/' => 'Facebook', 91 '/Instagram/' => 'Instagram', 92 '/TwitterAndroid|Twitter for iPhone/' => 'X', 93 '/\bGSA\//' => 'Google app', 94 '/SmartNews/i' => 'SmartNews', 95 '/Gunosy/i' => 'Gunosy', 96 '/NewsPicks/i' => 'NewsPicks', 97 '/TikTok|musical_ly|BytedanceWebview/i' => 'TikTok', 98 '/Pinterest/' => 'Pinterest', 99 '/MicroMessenger/' => 'WeChat', 100 '/Snapchat/' => 'Snapchat', 101 '/\bDuckDuckGo\//' => 'DuckDuckGo', 102 '/Mercari/i' => 'Mercari', 103 '/\bwv\)/' => 'Android WebView (other)', 104 ); 105 foreach ( $map as $re => $label ) { 106 if ( preg_match( $re, $ua ) ) { 107 return $label; 108 } 109 } 110 // iOS: Safari 本体には "Safari/" が付く。無い場合はアプリ内ブラウザー。 111 if ( false !== strpos( $ua, 'iPhone' ) || false !== strpos( $ua, 'iPad' ) ) { 112 if ( false === strpos( $ua, 'Safari/' ) && false === strpos( $ua, 'CriOS' ) && false === strpos( $ua, 'FxiOS' ) && false === strpos( $ua, 'EdgiOS' ) ) { 113 return 'iOS in-app (other)'; 114 } 115 } 116 return ''; 117 } 118 119 /** 79 120 * クエリ文字列の連想配列を列値に変換。 80 121 * … … 155 196 'network' => substr( sanitize_text_field( $row['network'] ), 0, 20 ), 156 197 'source' => substr( sanitize_text_field( $row['source'] ), 0, 40 ), 198 'app' => substr( self::app_from_ua( $row['user_agent'] ), 0, 40 ), 157 199 'extra' => substr( (string) $row['extra'], 0, 1000 ), 158 200 'repeat_ip' => $repeat_ip, … … 268 310 public static function breakdown( $column, $days, $limit = 20 ) { 269 311 global $wpdb; 270 if ( ! in_array( $column, array( 'campaign_id', 'placement', 'device', 'source', 'network', 'adgroup_id' ), true ) ) {312 if ( ! in_array( $column, array( 'campaign_id', 'placement', 'device', 'source', 'network', 'adgroup_id', 'app' ), true ) ) { 271 313 return array(); 272 314 } … … 368 410 369 411 /** 412 * 証跡エクスポート用: 期間内の bot(任意で保留も)着地。 413 * 414 * @param int $days 日数。 415 * @param bool $include_undetermined 保留も含める。 416 * @return array 417 */ 418 public static function evidence_rows( $days, $include_undetermined = false ) { 419 global $wpdb; 420 $table = self::table(); 421 $where = $include_undetermined ? "verdict IN ('bot','undetermined')" : "verdict = 'bot'"; 422 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 423 return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE created_at >= %s AND {$where} ORDER BY id ASC LIMIT 5000", self::since( $days ) ), ARRAY_A ); 424 } 425 426 /** 370 427 * URL から gclid 系の値を取り出す(クリックログとの突合用)。 371 428 * -
redkirin-ad-click-verification/trunk/includes/class-rkacv-admin.php
r3743325 r3743355 19 19 add_action( 'admin_post_rkacv_add_cidr', array( __CLASS__, 'handle_add_cidr' ) ); 20 20 add_action( 'admin_post_rkacv_clear_visits', array( __CLASS__, 'handle_clear_visits' ) ); 21 add_action( 'admin_post_rkacv_export', array( __CLASS__, 'handle_export' ) ); 21 22 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); 22 23 add_filter( 'plugin_action_links_' . plugin_basename( RKACV_PLUGIN_FILE ), array( __CLASS__, 'action_links' ) ); … … 76 77 RKACV_Ad_Visits::truncate(); 77 78 wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'tab' => 'ads', 'rkacv_msg' => 'visits_cleared' ), admin_url( 'options-general.php' ) ) ); 79 exit; 80 } 81 82 /** 83 * 証跡 CSV(無効クリックの申し立て用)。 84 */ 85 public static function handle_export() { 86 if ( ! current_user_can( 'manage_options' ) ) { 87 wp_die( esc_html__( 'You do not have permission to do this.', 'redkirin-ad-click-verification' ) ); 88 } 89 check_admin_referer( 'rkacv_export' ); 90 $days = isset( $_GET['days'] ) ? (int) $_GET['days'] : 7; 91 if ( ! in_array( $days, array( 7, 30, 90 ), true ) ) { 92 $days = 7; 93 } 94 $with_und = ! empty( $_GET['undetermined'] ); 95 96 nocache_headers(); 97 header( 'Content-Type: text/csv; charset=UTF-8' ); 98 header( 'Content-Disposition: attachment; filename="ad-click-verification-evidence-' . gmdate( 'Ymd' ) . '.csv"' ); 99 $out = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions 100 fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions 101 fputcsv( $out, array( 'datetime', 'type', 'verdict_or_reasons', 'click_id', 'campaign_id', 'placement', 'app', 'device', 'ip_anonymised', 'user_agent', 'page_url', 'dwell_ms', 'signals', 'link_url' ) ); 102 foreach ( RKACV_Ad_Visits::evidence_rows( $days, $with_und ) as $r ) { 103 fputcsv( $out, array( get_date_from_gmt( $r['created_at'], 'Y-m-d H:i:s' ), 'landing', $r['verdict'] . ( $r['reasons'] ? ' (' . $r['reasons'] . ')' : '' ), $r['gclid'], $r['campaign_id'], $r['placement'], $r['app'], $r['device'], $r['ip'], $r['user_agent'], $r['page_url'], $r['dwell_ms'], $r['signals'], '' ) ); 104 } 105 foreach ( RKACV_Log::evidence_rows( $days ) as $r ) { 106 fputcsv( $out, array( get_date_from_gmt( $r['created_at'], 'Y-m-d H:i:s' ), 'click', $r['reasons'], $r['click_id'], '', '', RKACV_Ad_Visits::app_from_ua( $r['user_agent'] ), '', $r['ip'], $r['user_agent'], $r['page_url'], $r['dwell_ms'], $r['signals'], $r['link_url'] ) ); 107 } 108 fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions 78 109 exit; 79 110 } … … 317 348 318 349 /** 基本設定タブの項目。 */ 319 const BASIC_KEYS = array( 'scope', 'verified_pageview', 'bot_tag_block', ' anonymize_ip', 'datalayer_event', 'debug', 'dedupe_daily_limit', 'dedupe_enabled', 'dedupe_window_min', 'dwell_signal_ms', 'enabled', 'gtag_direct', 'guard_datalayer', 'guarded_events', 'intercept_clicks', 'link_patterns', 'log_enabled', 'log_retention_days', 'log_verified', 'min_dwell_ms', 'min_signals' );350 const BASIC_KEYS = array( 'scope', 'verified_pageview', 'bot_tag_block', 'form_enabled', 'form_block_hard', 'form_exclude', 'thanks_paths', 'thanks_window_min', 'anonymize_ip', 'datalayer_event', 'debug', 'dedupe_daily_limit', 'dedupe_enabled', 'dedupe_window_min', 'dwell_signal_ms', 'enabled', 'gtag_direct', 'guard_datalayer', 'guarded_events', 'intercept_clicks', 'link_patterns', 'log_enabled', 'log_retention_days', 'log_verified', 'min_dwell_ms', 'min_signals' ); 320 351 321 352 /** 高度な設定タブの項目。 */ 322 const ADVANCED_KEYS = array( ' ad_capture_params', 'ad_report_delay_ms', 'ad_retention_days', 'ad_tracking', 'ad_trigger_params', 'bot_ua_patterns', 'deny_cidrs', 'deny_enabled', 'deny_ua', 'deny_ua_patterns', 'deny_webdriver', 'extra_cidrs', 'ip_check' );353 const ADVANCED_KEYS = array( 'cpc_default', 'cpc_by_campaign', 'currency_symbol', 'alert_enabled', 'alert_email', 'alert_bot_share', 'alert_min_landings', 'alert_repeat_clicks', 'alert_digest', 'ad_capture_params', 'ad_report_delay_ms', 'ad_retention_days', 'ad_tracking', 'ad_trigger_params', 'bot_ua_patterns', 'deny_cidrs', 'deny_enabled', 'deny_ua', 'deny_ua_patterns', 'deny_webdriver', 'extra_cidrs', 'ip_check' ); 323 354 324 355 private static function render_settings( $section = 'basic' ) { … … 347 378 self::field_number( $s, 'ad_report_delay_ms', __( 'Verdict delay (ms)', 'redkirin-ad-click-verification' ), __( 'How long to wait after the page starts loading before the landing is scored and sent. If the visitor leaves earlier, it is sent at that moment. Recommended 10000.', 'redkirin-ad-click-verification' ), 0, 120000, 'ad_tracking log_enabled' ); 348 379 self::field_number( $s, 'ad_retention_days', __( 'Ad landing retention (days)', 'redkirin-ad-click-verification' ), '', 1, 365, 'ad_tracking log_enabled' ); 380 ?> 381 </table> 382 383 <h2><?php esc_html_e( 'Wasted spend estimate', 'redkirin-ad-click-verification' ); ?></h2> 384 <table class="form-table" role="presentation"> 385 <?php 386 self::field_number( $s, 'cpc_default', __( 'Average CPC', 'redkirin-ad-click-verification' ), __( 'Average cost per click used for "bot landings x CPC" in the Ad traffic report and the dashboard. 0 hides the estimate.', 'redkirin-ad-click-verification' ), 0, 1000000 ); 387 self::field_textarea( $s, 'cpc_by_campaign', __( 'CPC per campaign', 'redkirin-ad-click-verification' ), __( 'One line per campaign as <code>campaign_id=cpc</code>, e.g. <code>21505995745=180</code>. Campaigns not listed use the average CPC.', 'redkirin-ad-click-verification' ), 4 ); 388 self::field_text( $s, 'currency_symbol', __( 'Currency symbol', 'redkirin-ad-click-verification' ) ); 389 ?> 390 </table> 391 392 <h2><?php esc_html_e( 'Alerts', 'redkirin-ad-click-verification' ); ?></h2> 393 <table class="form-table" role="presentation"> 394 <?php 395 self::field_checkbox( $s, 'alert_enabled', __( 'Send alert emails', 'redkirin-ad-click-verification' ), __( 'Checks the last 24 hours once an hour (WP-Cron) and emails when the bot share of ad landings or repeated conversions from one click ID exceed the thresholds. Each condition is reported at most once per 24 hours.', 'redkirin-ad-click-verification' ) ); 396 self::field_text( $s, 'alert_email', __( 'Recipient', 'redkirin-ad-click-verification' ), __( 'Leave empty to use the site admin email.', 'redkirin-ad-click-verification' ), 'alert_enabled' ); 397 self::field_number( $s, 'alert_bot_share', __( 'Bot share threshold (%)', 'redkirin-ad-click-verification' ), '', 1, 100, 'alert_enabled' ); 398 self::field_number( $s, 'alert_min_landings', __( 'Minimum landings for the share alert', 'redkirin-ad-click-verification' ), '', 1, 100000, 'alert_enabled' ); 399 self::field_number( $s, 'alert_repeat_clicks', __( 'Repeated conversions per click ID', 'redkirin-ad-click-verification' ), '', 2, 1000, 'alert_enabled' ); 400 self::field_checkbox( $s, 'alert_digest', __( 'Daily digest', 'redkirin-ad-click-verification' ), __( 'Once a day: blocked / verified clicks, ad landings and the wasted spend estimate.', 'redkirin-ad-click-verification' ), 'alert_enabled' ); 349 401 ?> 350 402 </table> … … 406 458 self::field_number( $s, 'dedupe_window_min', __( 'Window (minutes)', 'redkirin-ad-click-verification' ), __( 'Within this window, the same link type is measured only once per click ID / session. 0 disables the window rule.', 'redkirin-ad-click-verification' ), 0, 1440, 'dedupe_enabled' ); 407 459 self::field_number( $s, 'dedupe_daily_limit', __( 'Daily limit per click ID', 'redkirin-ad-click-verification' ), __( 'Maximum verified clicks per click ID in 24 hours, checked against the log via the REST API. 0 disables the limit.', 'redkirin-ad-click-verification' ), 0, 100, 'dedupe_enabled' ); 460 ?> 461 </table> 462 463 <h2><?php esc_html_e( 'Form submissions and thank-you pages', 'redkirin-ad-click-verification' ); ?></h2> 464 <table class="form-table" role="presentation"> 465 <?php 466 self::field_checkbox( $s, 'form_enabled', __( 'Verify form submissions', 'redkirin-ad-click-verification' ), __( 'Applies the same human verification to form submissions. Every submission gets hidden fields <code>rkacv_verdict</code> (human / undetermined / bot), <code>rkacv_reasons</code>, <code>rkacv_dwell</code> and <code>rkacv_signals</code>, so your CRM or form plugin can filter them. Verified submissions push <code>rkacv_form_submit</code> to the dataLayer; use it as the form key event.', 'redkirin-ad-click-verification' ) ); 467 self::field_checkbox( $s, 'form_block_hard', __( 'Block submissions with definite bot signals', 'redkirin-ad-click-verification' ), __( 'Stops the submission in the browser when webdriver, a bot user agent, automation globals or a synthetic submit event are present. Contact Form 7 marks such submissions as spam and WPForms rejects them on the server as well. Submissions that merely lack dwell time or interaction are never blocked.', 'redkirin-ad-click-verification' ), 'form_enabled' ); 468 self::field_textarea( $s, 'form_exclude', __( 'Forms to ignore', 'redkirin-ad-click-verification' ), __( 'CSS selectors, one per line. Matching forms (or forms inside a matching element) are not verified.', 'redkirin-ad-click-verification' ), 4, 'form_enabled' ); 469 self::field_textarea( $s, 'thanks_paths', __( 'Thank-you page paths', 'redkirin-ad-click-verification' ), __( 'One path per line, e.g. <code>/thanks/</code> or <code>/contact/complete/</code>. On these pages the verified page view event is pushed only if a verified form was submitted in this browser session within the window below, and <code>rkacv_thanks_view</code> is pushed as well. Direct visits and crawlers hitting the thank-you URL are logged and not counted. Leave empty to disable.', 'redkirin-ad-click-verification' ), 3, 'form_enabled' ); 470 self::field_number( $s, 'thanks_window_min', __( 'Thank-you window (minutes)', 'redkirin-ad-click-verification' ), '', 1, 1440, 'form_enabled' ); 408 471 ?> 409 472 </table> … … 626 689 'repeat_limit' => __( 'Repeat: daily limit per click ID', 'redkirin-ad-click-verification' ), 627 690 'key_on_touch' => __( 'Note: keyboard on touch device', 'redkirin-ad-click-verification' ), 691 'no_form_submit' => __( 'Thank-you page without a verified form submission', 'redkirin-ad-click-verification' ), 628 692 'deny_ua' => __( 'Denied: user agent', 'redkirin-ad-click-verification' ), 629 693 'deny_ip' => __( 'Denied: IP range', 'redkirin-ad-click-verification' ), … … 646 710 } 647 711 648 private static function breakdown_table( $title, $rows, $cv_by_key = array(), $key_label = '' ) {712 private static function breakdown_table( $title, $rows, $cv_by_key = array(), $key_label = '', $waste_by_key = null, $currency = '' ) { 649 713 if ( empty( $rows ) ) { 650 714 return; … … 662 726 <th><?php esc_html_e( 'Bot share', 'redkirin-ad-click-verification' ); ?></th> 663 727 <?php if ( $cv_by_key ) : ?><th class="rkacv-num"><?php esc_html_e( 'Verified clicks', 'redkirin-ad-click-verification' ); ?></th><?php endif; ?> 728 <?php if ( null !== $waste_by_key ) : ?><th class="rkacv-num"><?php esc_html_e( 'Est. waste', 'redkirin-ad-click-verification' ); ?></th><?php endif; ?> 664 729 </tr> 665 730 </thead> … … 680 745 </td> 681 746 <?php if ( $cv_by_key ) : ?><td class="rkacv-num"><?php echo esc_html( number_format_i18n( isset( $cv_by_key[ $r['key'] ] ) ? $cv_by_key[ $r['key'] ] : 0 ) ); ?></td><?php endif; ?> 747 <?php if ( null !== $waste_by_key ) : ?><td class="rkacv-num"><?php echo esc_html( $currency . number_format_i18n( isset( $waste_by_key[ $r['key'] ] ) ? $waste_by_key[ $r['key'] ] : 0 ) ); ?></td><?php endif; ?> 682 748 </tr> 683 749 <?php endforeach; ?> … … 705 771 $by_dev = RKACV_Ad_Visits::breakdown( 'device', $days ); 706 772 $by_src = RKACV_Ad_Visits::breakdown( 'source', $days ); 773 $by_app = RKACV_Ad_Visits::breakdown( 'app', $days ); 774 $cpc = RKACV_Settings::cpc_table( $s ); 775 $has_cpc = $cpc['default'] > 0 || ! empty( $cpc['by'] ); 776 $waste_by_cmp = array(); 777 $waste_total = 0; 778 if ( $has_cpc ) { 779 foreach ( $by_cmp as $c ) { 780 $rate = isset( $cpc['by'][ $c['key'] ] ) ? $cpc['by'][ $c['key'] ] : $cpc['default']; 781 $waste_by_cmp[ $c['key'] ] = $c['bot'] * $rate; 782 $waste_total += $c['bot'] * $rate; 783 } 784 } 785 $export_url = wp_nonce_url( admin_url( 'admin-post.php?action=rkacv_export&days=' . $days ), 'rkacv_export' ); 707 786 $ips = RKACV_Ad_Visits::suspicious_ips( $days, 2 ); 708 787 $recent = RKACV_Ad_Visits::recent( 50 ); … … 726 805 <?php endforeach; ?> 727 806 </ul> 807 <div class="rkacv-toolbar-right"> 808 <a class="button" href="<?php echo esc_url( $export_url ); ?>"><?php esc_html_e( 'Export evidence CSV', 'redkirin-ad-click-verification' ); ?></a> 809 <a class="button" href="<?php echo esc_url( $export_url . '&undetermined=1' ); ?>" title="<?php esc_attr_e( 'Includes undetermined landings as well as bot signals', 'redkirin-ad-click-verification' ); ?>"><?php esc_html_e( 'Export incl. undetermined', 'redkirin-ad-click-verification' ); ?></a> 728 810 <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" onsubmit="return confirm('<?php echo esc_js( __( 'Delete all ad landing records?', 'redkirin-ad-click-verification' ) ); ?>');"> 729 811 <input type="hidden" name="action" value="rkacv_clear_visits"> … … 731 813 <button class="button"><?php esc_html_e( 'Clear records', 'redkirin-ad-click-verification' ); ?></button> 732 814 </form> 815 </div> 733 816 </div> 817 <p class="description"><?php esc_html_e( 'The evidence CSV (bot landings and blocked clicks with time, click ID, campaign, app, user agent and reasons) can be attached to the Google Ads invalid click claim form.', 'redkirin-ad-click-verification' ); ?></p> 734 818 735 819 <div class="rkacv-cards"> 820 <?php if ( $has_cpc ) : ?> 821 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Estimated wasted spend', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value rkacv-blocked"><?php echo esc_html( $s['currency_symbol'] . number_format_i18n( $waste_total ) ); ?></div><div class="rkacv-card-sub"><?php esc_html_e( 'bot landings x CPC', 'redkirin-ad-click-verification' ); ?></div></div> 822 <?php endif; ?> 736 823 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Ad landings', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value"><?php echo esc_html( number_format_i18n( $sum['total'] ) ); ?></div></div> 737 824 <div class="rkacv-card"><div class="rkacv-card-label"><?php esc_html_e( 'Bot signals', 'redkirin-ad-click-verification' ); ?></div><div class="rkacv-card-value rkacv-blocked"><?php echo esc_html( number_format_i18n( $sum['bot'] ) ); ?> <small><?php echo esc_html( self::pct( $sum['bot'], $sum['total'] ) ); ?>%</small></div></div> … … 747 834 748 835 <?php 749 self::breakdown_table( __( 'By campaign', 'redkirin-ad-click-verification' ), $by_cmp, $cv_by_cmp, __( 'Campaign ID', 'redkirin-ad-click-verification' ) ); 836 self::breakdown_table( __( 'By campaign', 'redkirin-ad-click-verification' ), $by_cmp, $cv_by_cmp, __( 'Campaign ID', 'redkirin-ad-click-verification' ), $has_cpc ? $waste_by_cmp : null, $s['currency_symbol'] ); 837 self::breakdown_table( __( 'By app (in-app browser)', 'redkirin-ad-click-verification' ), $by_app, array(), __( 'App', 'redkirin-ad-click-verification' ) ); 750 838 self::breakdown_table( __( 'By placement', 'redkirin-ad-click-verification' ), $by_pl, array(), __( 'Placement', 'redkirin-ad-click-verification' ) ); 751 839 self::breakdown_table( __( 'By device', 'redkirin-ad-click-verification' ), $by_dev, array(), __( 'Device', 'redkirin-ad-click-verification' ) ); … … 806 894 <th><?php esc_html_e( 'Campaign ID', 'redkirin-ad-click-verification' ); ?></th> 807 895 <th><?php esc_html_e( 'Placement', 'redkirin-ad-click-verification' ); ?></th> 896 <th><?php esc_html_e( 'App', 'redkirin-ad-click-verification' ); ?></th> 808 897 <th><?php esc_html_e( 'Reason', 'redkirin-ad-click-verification' ); ?></th> 809 898 <th><?php esc_html_e( 'Dwell / signals', 'redkirin-ad-click-verification' ); ?></th> … … 828 917 <td class="rkacv-ellipsis"><?php echo esc_html( $r['campaign_id'] ); ?></td> 829 918 <td class="rkacv-ellipsis" title="<?php echo esc_attr( $r['placement'] ); ?>"><?php echo esc_html( $r['placement'] ); ?></td> 919 <td class="rkacv-ellipsis"><?php echo esc_html( isset( $r['app'] ) ? $r['app'] : '' ); ?></td> 830 920 <td><?php echo esc_html( self::reasons_label( $r['reasons'] ) ); ?></td> 831 921 <td class="rkacv-nowrap"><?php echo esc_html( $r['dwell_ms'] ); ?>ms / <?php echo esc_html( $r['signals'] ); ?></td> -
redkirin-ad-click-verification/trunk/includes/class-rkacv-dashboard.php
r3743244 r3743355 87 87 $bot_pct = round( $ads['bot'] / $ads['total'] * 100 ); 88 88 echo esc_html( sprintf( /* translators: 1: landings, 2: bot percentage, 3: undetermined count */ __( 'Ad landings: %1$d, %2$d%% with bot signals, %3$d undetermined', 'redkirin-ad-click-verification' ), $ads['total'], $bot_pct, $ads['undetermined'] ) ); 89 $cpc = RKACV_Settings::cpc_table( $settings ); 90 if ( $cpc['default'] > 0 || $cpc['by'] ) { 91 $waste = 0; 92 foreach ( RKACV_Ad_Visits::breakdown( 'campaign_id', self::DAYS, 100 ) as $c ) { 93 $waste += $c['bot'] * ( isset( $cpc['by'][ $c['key'] ] ) ? $cpc['by'][ $c['key'] ] : $cpc['default'] ); 94 } 95 echo ' · ' . esc_html( sprintf( /* translators: 1: currency, 2: amount */ __( 'est. wasted spend %1$s%2$s', 'redkirin-ad-click-verification' ), $settings['currency_symbol'], number_format_i18n( $waste ) ) ); 96 } 89 97 ?> 90 98 <a href="<?php echo esc_url( admin_url( 'options-general.php?page=' . RKACV_Admin::PAGE . '&tab=ads' ) ); ?>"><?php esc_html_e( 'Report', 'redkirin-ad-click-verification' ); ?></a> -
redkirin-ad-click-verification/trunk/includes/class-rkacv-frontend.php
r3743325 r3743355 50 50 'debug' => ! empty( $settings['debug'] ), 51 51 'denyWebdriver' => ! empty( $settings['deny_enabled'] ) && ! empty( $settings['deny_webdriver'] ), 52 'forms' => ! empty( $settings['form_enabled'] ), 53 'formBlockHard' => ! empty( $settings['form_block_hard'] ), 54 'formExclude' => RKACV_Settings::lines( $settings['form_exclude'] ), 55 'thanksPaths' => RKACV_Settings::lines( $settings['thanks_paths'] ), 56 'thanksWindow' => (int) $settings['thanks_window_min'] * 60 * 1000, 52 57 'verifiedPV' => ! empty( $settings['verified_pageview'] ), 53 58 'botTagBlock' => ! empty( $settings['bot_tag_block'] ), -
redkirin-ad-click-verification/trunk/includes/class-rkacv-log.php
r3743265 r3743355 10 10 class RKACV_Log { 11 11 12 const DB_VERSION = ' 4';12 const DB_VERSION = '5'; 13 13 14 14 /** 保存する最大行数。超えた分は古い行から削除する(リングバッファ)。 */ … … 103 103 } 104 104 return $ok; 105 } 106 107 /** 108 * 証跡エクスポート用: 期間内の遮断クリック。 109 * 110 * @param int $days 日数。 111 * @return array 112 */ 113 public static function evidence_rows( $days ) { 114 global $wpdb; 115 $table = self::table(); 116 $since = gmdate( 'Y-m-d H:i:s', time() - (int) $days * DAY_IN_SECONDS ); 117 // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared 118 return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE created_at >= %s AND status = 'blocked' ORDER BY id ASC LIMIT 5000", $since ), ARRAY_A ); 105 119 } 106 120 -
redkirin-ad-click-verification/trunk/includes/class-rkacv-settings.php
r3743325 r3743355 104 104 'dedupe_window_min' => 30, 105 105 'dedupe_daily_limit' => 3, 106 // 無駄費用の推定(bot 着地 × CPC)。 107 'cpc_default' => 0, 108 'cpc_by_campaign' => '', 109 'currency_symbol' => '¥', 110 // フォーム送信の保護。 111 'form_enabled' => 1, 112 'form_block_hard' => 1, 113 'form_exclude' => "form[role=search]\n.search-form\n#loginform\n#adminbarsearch", 114 // サンクスページの到達検証(空なら無効)。 115 'thanks_paths' => '', 116 'thanks_window_min' => 30, 117 // 異常検知アラート。 118 'alert_enabled' => 0, 119 'alert_email' => '', 120 'alert_bot_share' => 30, 121 'alert_min_landings' => 20, 122 'alert_repeat_clicks' => 5, 123 'alert_digest' => 0, 106 124 // 広告着地の記録(アドフラウド可視化)。 107 125 'ad_tracking' => 1, … … 182 200 $out = array(); 183 201 184 foreach ( array( 'enabled', 'intercept_clicks', 'guard_datalayer', 'gtag_direct', 'ip_check', 'log_enabled', 'log_verified', 'anonymize_ip', 'debug', 'deny_enabled', 'deny_cidrs', 'deny_ua', 'deny_webdriver', 'ad_tracking', 'dedupe_enabled', 'verified_pageview', 'bot_tag_block' ) as $k ) {202 foreach ( array( 'enabled', 'intercept_clicks', 'guard_datalayer', 'gtag_direct', 'ip_check', 'log_enabled', 'log_verified', 'anonymize_ip', 'debug', 'deny_enabled', 'deny_cidrs', 'deny_ua', 'deny_webdriver', 'ad_tracking', 'dedupe_enabled', 'verified_pageview', 'bot_tag_block', 'form_enabled', 'form_block_hard', 'alert_enabled', 'alert_digest' ) as $k ) { 185 203 $out[ $k ] = empty( $input[ $k ] ) ? 0 : 1; 186 204 } … … 190 208 $out['dwell_signal_ms'] = isset( $input['dwell_signal_ms'] ) ? max( 0, min( 600000, (int) $input['dwell_signal_ms'] ) ) : $d['dwell_signal_ms']; 191 209 $out['log_retention_days'] = isset( $input['log_retention_days'] ) ? max( 1, min( 365, (int) $input['log_retention_days'] ) ) : $d['log_retention_days']; 210 $out['cpc_default'] = isset( $input['cpc_default'] ) ? max( 0, (float) $input['cpc_default'] ) : $d['cpc_default']; 211 $out['thanks_window_min'] = isset( $input['thanks_window_min'] ) ? max( 1, min( 1440, (int) $input['thanks_window_min'] ) ) : $d['thanks_window_min']; 212 $out['alert_bot_share'] = isset( $input['alert_bot_share'] ) ? max( 1, min( 100, (int) $input['alert_bot_share'] ) ) : $d['alert_bot_share']; 213 $out['alert_min_landings'] = isset( $input['alert_min_landings'] ) ? max( 1, min( 100000, (int) $input['alert_min_landings'] ) ) : $d['alert_min_landings']; 214 $out['alert_repeat_clicks'] = isset( $input['alert_repeat_clicks'] ) ? max( 2, min( 1000, (int) $input['alert_repeat_clicks'] ) ) : $d['alert_repeat_clicks']; 215 $out['currency_symbol'] = isset( $input['currency_symbol'] ) ? substr( sanitize_text_field( $input['currency_symbol'] ), 0, 5 ) : $d['currency_symbol']; 216 $out['alert_email'] = isset( $input['alert_email'] ) ? sanitize_text_field( $input['alert_email'] ) : $d['alert_email']; 192 217 $out['dedupe_window_min'] = isset( $input['dedupe_window_min'] ) ? max( 0, min( 1440, (int) $input['dedupe_window_min'] ) ) : $d['dedupe_window_min']; 193 218 $out['dedupe_daily_limit'] = isset( $input['dedupe_daily_limit'] ) ? max( 0, min( 100, (int) $input['dedupe_daily_limit'] ) ) : $d['dedupe_daily_limit']; … … 195 220 $out['ad_retention_days'] = isset( $input['ad_retention_days'] ) ? max( 1, min( 365, (int) $input['ad_retention_days'] ) ) : $d['ad_retention_days']; 196 221 197 foreach ( array( 'link_patterns', 'guarded_events', 'bot_ua_patterns', 'extra_cidrs', 'deny_ua_patterns', 'ad_trigger_params', 'ad_capture_params' ) as $k ) {222 foreach ( array( 'link_patterns', 'guarded_events', 'bot_ua_patterns', 'extra_cidrs', 'deny_ua_patterns', 'ad_trigger_params', 'ad_capture_params', 'cpc_by_campaign', 'form_exclude', 'thanks_paths' ) as $k ) { 198 223 $out[ $k ] = isset( $input[ $k ] ) ? self::sanitize_lines( $input[ $k ] ) : $d[ $k ]; 199 224 } … … 240 265 241 266 /** 267 * キャンペーン ID ごとの CPC("id=cpc" 行)とデフォルト CPC。 268 * 269 * @param array $settings 設定。 270 * @return array{default:float,by:array<string,float>} 271 */ 272 public static function cpc_table( $settings ) { 273 $by = array(); 274 foreach ( self::lines( $settings['cpc_by_campaign'] ) as $line ) { 275 if ( preg_match( '/^\s*([^=\s]+)\s*=\s*([0-9.]+)\s*$/', $line, $m ) ) { 276 $by[ $m[1] ] = (float) $m[2]; 277 } 278 } 279 return array( 280 'default' => (float) $settings['cpc_default'], 281 'by' => $by, 282 ); 283 } 284 285 /** 242 286 * 改行区切りテキストの整形。 243 287 * -
redkirin-ad-click-verification/trunk/languages/redkirin-ad-click-verification-ja.po
r3743325 r3743355 2 2 msgid "" 3 3 msgstr "" 4 "Project-Id-Version: Redkirin Ad Click Verification 1. 2.0\n"4 "Project-Id-Version: Redkirin Ad Click Verification 1.3.0\n" 5 5 "POT-Creation-Date: 2026-10-12 00:08+0000\n" 6 6 "PO-Revision-Date: 2026-10-12 00:08+0000\n" … … 683 683 msgstr "別案として、page_view のキーイベントを残したまま GA4 設定タグに例外トリガー「rkacv_bot 等しい true」を追加する方法もあります。ただし読込時点で分かる bot にしか効きません。" 684 684 685 msgid "Wasted spend estimate" 686 msgstr "無駄費用の推定" 687 688 msgid "Average CPC" 689 msgstr "平均 CPC" 690 691 msgid "Average cost per click used for \"bot landings x CPC\" in the Ad traffic report and the dashboard. 0 hides the estimate." 692 msgstr "広告トラフィックのレポートとダッシュボードで「bot 着地 × CPC」の計算に使う平均クリック単価。0 で非表示。" 693 694 msgid "CPC per campaign" 695 msgstr "キャンペーン別 CPC" 696 697 msgid "One line per campaign as <code>campaign_id=cpc</code>, e.g. <code>21505995745=180</code>. Campaigns not listed use the average CPC." 698 msgstr "1 行 1 キャンペーンで <code>キャンペーンID=CPC</code> の形式(例: <code>21505995745=180</code>)。未記載のキャンペーンは平均 CPC を使います。" 699 700 msgid "Currency symbol" 701 msgstr "通貨記号" 702 703 msgid "Alerts" 704 msgstr "アラート" 705 706 msgid "Send alert emails" 707 msgstr "アラートメールを送る" 708 709 msgid "Checks the last 24 hours once an hour (WP-Cron) and emails when the bot share of ad landings or repeated conversions from one click ID exceed the thresholds. Each condition is reported at most once per 24 hours." 710 msgstr "1 時間ごと(WP-Cron)に直近 24 時間を確認し、広告着地の bot 率や同一クリック ID からの反復コンバージョンがしきい値を超えたらメールします。同じ条件の通知は 24 時間に 1 回までです。" 711 712 msgid "Recipient" 713 msgstr "宛先" 714 715 msgid "Leave empty to use the site admin email." 716 msgstr "空ならサイトの管理者メールアドレスを使います。" 717 718 msgid "Bot share threshold (%)" 719 msgstr "bot 率のしきい値 (%)" 720 721 msgid "Minimum landings for the share alert" 722 msgstr "bot 率アラートに必要な最低着地数" 723 724 msgid "Repeated conversions per click ID" 725 msgstr "クリック ID ごとの反復コンバージョン数" 726 727 msgid "Daily digest" 728 msgstr "日次ダイジェスト" 729 730 msgid "Once a day: blocked / verified clicks, ad landings and the wasted spend estimate." 731 msgstr "1 日 1 回、遮断 / 検証済みクリック、広告着地、無駄費用の推定を送ります。" 732 733 msgid "Form submissions and thank-you pages" 734 msgstr "フォーム送信とサンクスページ" 735 736 msgid "Verify form submissions" 737 msgstr "フォーム送信を検証する" 738 739 msgid "Applies the same human verification to form submissions. Every submission gets hidden fields <code>rkacv_verdict</code> (human / undetermined / bot), <code>rkacv_reasons</code>, <code>rkacv_dwell</code> and <code>rkacv_signals</code>, so your CRM or form plugin can filter them. Verified submissions push <code>rkacv_form_submit</code> to the dataLayer; use it as the form key event." 740 msgstr "フォーム送信にも同じ人間判定を適用します。すべての送信に隠しフィールド <code>rkacv_verdict</code>(human / undetermined / bot)、<code>rkacv_reasons</code>、<code>rkacv_dwell</code>、<code>rkacv_signals</code> が付くので、CRM やフォームプラグイン側で判別できます。検証済みの送信は <code>rkacv_form_submit</code> を dataLayer に push します。フォームのキーイベントにはこれを使ってください。" 741 742 msgid "Block submissions with definite bot signals" 743 msgstr "bot 兆候が明確な送信を止める" 744 745 msgid "Stops the submission in the browser when webdriver, a bot user agent, automation globals or a synthetic submit event are present. Contact Form 7 marks such submissions as spam and WPForms rejects them on the server as well. Submissions that merely lack dwell time or interaction are never blocked." 746 msgstr "webdriver・bot UA・自動化ツールの痕跡・合成 submit イベントがある場合、ブラウザー側で送信を止めます。Contact Form 7 では迷惑メール扱い、WPForms ではサーバー側でもエラーにします。滞在時間や操作が足りないだけの送信は止めません。" 747 748 msgid "Forms to ignore" 749 msgstr "対象外にするフォーム" 750 751 msgid "CSS selectors, one per line. Matching forms (or forms inside a matching element) are not verified." 752 msgstr "CSS セレクタを 1 行 1 件。一致するフォーム(または一致する要素内のフォーム)は検証しません。" 753 754 msgid "Thank-you page paths" 755 msgstr "サンクスページのパス" 756 757 msgid "One path per line, e.g. <code>/thanks/</code> or <code>/contact/complete/</code>. On these pages the verified page view event is pushed only if a verified form was submitted in this browser session within the window below, and <code>rkacv_thanks_view</code> is pushed as well. Direct visits and crawlers hitting the thank-you URL are logged and not counted. Leave empty to disable." 758 msgstr "1 行 1 パス(例: <code>/thanks/</code>、<code>/contact/complete/</code>)。これらのページでは、同じブラウザーセッションで下の時間内に検証済みのフォーム送信があった場合だけ検証済みページビューを push し、あわせて <code>rkacv_thanks_view</code> も push します。URL 直打ちやクローラーの到達はログに残し計測しません。空で無効。" 759 760 msgid "Thank-you window (minutes)" 761 msgstr "サンクスページの有効時間(分)" 762 763 msgid "Thank-you page without a verified form submission" 764 msgstr "検証済みフォーム送信のないサンクスページ到達" 765 766 msgid "Est. waste" 767 msgstr "推定無駄費用" 768 769 msgid "Export evidence CSV" 770 msgstr "証跡 CSV を出力" 771 772 msgid "Includes undetermined landings as well as bot signals" 773 msgstr "bot 兆候ありに加えて保留の着地も含めます" 774 775 msgid "Export incl. undetermined" 776 msgstr "保留を含めて出力" 777 778 msgid "The evidence CSV (bot landings and blocked clicks with time, click ID, campaign, app, user agent and reasons) can be attached to the Google Ads invalid click claim form." 779 msgstr "証跡 CSV(bot 着地と遮断クリック: 日時、クリック ID、キャンペーン、アプリ、UA、理由)は Google 広告の無効なクリックの申し立てフォームに添付できます。" 780 781 msgid "Estimated wasted spend" 782 msgstr "推定無駄費用" 783 784 msgid "bot landings x CPC" 785 msgstr "bot 着地 × CPC" 786 787 msgid "By app (in-app browser)" 788 msgstr "流入アプリ別(アプリ内ブラウザー)" 789 790 msgid "App" 791 msgstr "アプリ" 792 793 msgid "Bot share of ad landings in the last 24 hours: %1$d%% (%2$d landings, %3$d with bot signals)." 794 msgstr "直近 24 時間の広告着地の bot 率: %1$d%%(着地 %2$d 件、bot 兆候あり %3$d 件)。" 795 796 msgid "Campaign %1$s: %2$d bot / %3$d landings" 797 msgstr "キャンペーン %1$s: bot %2$d / 着地 %3$d" 798 799 msgid "Repeated conversions from one ad click ID %1$s: %2$d verified, %3$d repeat-blocked, %4$d IPs in the last 24 hours." 800 msgstr "1 つの広告クリック ID %1$s から反復コンバージョン: 直近 24 時間で検証済み %2$d 件、反復遮断 %3$d 件、IP 数 %4$d。" 801 802 msgid "Ad Click Verification alert" 803 msgstr "Ad Click Verification アラート" 804 805 msgid "Clicks in the last 24 hours: %1$d blocked (%2$d with bot signals), %3$d verified, %4$d page requests denied." 806 msgstr "直近 24 時間のクリック: 遮断 %1$d 件(bot 兆候あり %2$d 件)、検証済み %3$d 件、表示拒否 %4$d 件。" 807 808 msgid "Ad landings: %1$d (%2$d bot signals, %3$d undetermined, %4$d human)." 809 msgstr "広告着地: %1$d 件(bot 兆候あり %2$d、保留 %3$d、人間 %4$d)。" 810 811 msgid "Estimated wasted spend (bot landings x CPC): %1$s%2$s" 812 msgstr "推定無駄費用(bot 着地 × CPC): %1$s%2$s" 813 814 msgid "Ad Click Verification daily digest" 815 msgstr "Ad Click Verification 日次ダイジェスト" 816 817 msgid "est. wasted spend %1$s%2$s" 818 msgstr "推定無駄費用 %1$s%2$s" 819 820 msgid "The submission could not be verified. Please try again." 821 msgstr "送信を確認できませんでした。もう一度お試しください。" 822 -
redkirin-ad-click-verification/trunk/readme.txt
r3743325 r3743355 5 5 Tested up to: 7.1 6 6 Requires PHP: 7.4 7 Stable tag: 1. 2.07 Stable tag: 1.3.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 21 21 * **Click interception** – unverified clicks are stopped at the `window` capture phase, before Google Tag Manager click triggers or inline `onclick` handlers see them. The navigation itself (dialing, opening LINE, following the link) is performed by the plugin, so visitors are never blocked. 22 22 * **dataLayer guard** – `gtm.linkClick` / `gtm.click` and any event names you specify are dropped from `dataLayer.push` while the visitor is unverified. 23 * **Form submission verification** – the same human verification applies to form submissions. Hidden fields carry the verdict for your CRM, verified submissions push <code>rkacv_form_submit</code>, submissions with definite bot signals are stopped (Contact Form 7 marks them as spam, WPForms rejects them), and thank-you pages only count when a verified form was submitted in the same session. 24 * **In-app browser detection** – landings are classified by app (Yahoo! JAPAN, LINE, Facebook, Instagram, X, Google app, TikTok, generic WebView, ...), so you can see which apps bot traffic comes from even without ValueTrack placements. 25 * **Wasted spend estimate** – enter an average CPC (or per campaign) and the report shows bot landings x CPC per campaign and in total. 26 * **Evidence export** – CSV of bot landings and blocked clicks for the Google Ads invalid click claim form. 27 * **Alerts** – hourly check with email when the bot share of ad landings or repeated conversions from one click ID exceed your thresholds; optional daily digest. 23 28 * **Page view protection** – obvious bots (webdriver, bot user agents, automation globals) get no tags at all: every dataLayer push is dropped, so GTM and gtag.js send nothing, not even page_view. Once a visitor is verified, a <code>rkacv_verified_pageview</code> event is pushed that you can use as a key event instead of page_view (thank-you pages, landing pages). 24 29 * **Verified events** – verified clicks push `rkacv_verified_click` to the dataLayer (with `rkacv_link_type`, `rkacv_link_url`, `rkacv_link_text`, `rkacv_element_id`), and can optionally call `gtag('event', ...)` directly. … … 94 99 == Changelog == 95 100 101 = 1.3.0 = 102 * In-app browser classification for ad landings (new "By app" breakdown and column); evidence CSV export for Google Ads invalid click claims; wasted spend estimate (bot landings x CPC, per campaign CPC overrides) in the report, the dashboard and the digest. 103 * Form submission verification: hidden verdict fields, rkacv_form_submit event for verified submissions, blocking of submissions with definite bot signals (browser, Contact Form 7 spam, WPForms error), configurable excluded forms. 104 * Thank-you page gating: verified page view / rkacv_thanks_view only after a verified form submission in the same session; direct hits are logged. 105 * Alert emails (hourly check: bot share, repeated conversions per click ID) and an optional daily digest. 106 96 107 = 1.2.0 = 97 108 * New "Fire no tags for obvious bots" (default on): when webdriver, a bot user agent or automation globals are present at load, every dataLayer push is dropped so GTM / gtag.js send nothing, including page_view. -
redkirin-ad-click-verification/trunk/redkirin-ad-click-verification.php
r3743325 r3743355 3 3 * Plugin Name: Redkirin Ad Click Verification 4 4 * Description: Stops bots from inflating GA4 events and Google Ads conversions by verifying that clicks on phone, LINE, email and other tracked links come from a real person. 5 * Version: 1. 2.05 * Version: 1.3.0 6 6 * Author: Redkirin 7 7 * Author URI: https://redkirin.co.jp/ … … 17 17 } 18 18 19 define( 'RKACV_VERSION', '1. 2.0' );19 define( 'RKACV_VERSION', '1.3.0' ); 20 20 define( 'RKACV_PLUGIN_FILE', __FILE__ ); 21 21 define( 'RKACV_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); … … 30 30 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-frontend.php'; 31 31 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-deny.php'; 32 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-forms.php'; 33 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-alerts.php'; 32 34 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-admin.php'; 33 35 require_once RKACV_PLUGIN_DIR . 'includes/class-rkacv-dashboard.php'; … … 43 45 wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'rkacv_daily_maintenance' ); 44 46 } 47 RKACV_Alerts::schedule(); 45 48 46 49 $settings = RKACV_Settings::get(); … … 56 59 function rkacv_deactivate() { 57 60 wp_clear_scheduled_hook( 'rkacv_daily_maintenance' ); 61 RKACV_Alerts::unschedule(); 58 62 } 59 63 register_deactivation_hook( __FILE__, 'rkacv_deactivate' ); … … 90 94 RKACV_Frontend::init(); 91 95 RKACV_Deny::init(); 96 RKACV_Forms::init(); 97 RKACV_Alerts::init(); 98 RKACV_Alerts::schedule(); 92 99 if ( is_admin() ) { 93 100 RKACV_Admin::init(); -
redkirin-ad-click-verification/trunk/uninstall.php
r3743244 r3743355 27 27 delete_transient( 'rkacv_ip_refresh_lock' ); 28 28 delete_transient( 'rkacv_rl_global' ); 29 wp_clear_scheduled_hook( 'rkacv_hourly_check' ); 30 delete_transient( 'rkacv_alert_share' );
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)