File "class-analytics.php"
Full path: /home/webcknlt/admissiontell.com/wp-content/plugins/vibes/includes/features/class-analytics.php
File
size: 92.62 B (92.62 KB bytes)
MIME-type: text/x-php
Charset: utf-8
Download Open Edit Advanced Editor &nnbsp; Back
<?php
/**
* Vibes analytics
*
* Handles all analytics operations.
*
* @package Features
* @author Pierre Lannoy <https://pierre.lannoy.fr/>.
* @since 1.0.0
*/
namespace Vibes\Plugin\Feature;
use Vibes\Plugin\Feature\Schema;
use Vibes\System\Blog;
use Vibes\System\Cache;
use Vibes\System\Date;
use Vibes\System\Conversion;
use Vibes\System\Device;
use Vibes\System\Mime;
use Vibes\System\Option;
use Vibes\System\Role;
use Vibes\System\L10n;
use Vibes\System\Http;
use Vibes\System\Favicon;
use Vibes\System\Timezone;
use Vibes\System\UUID;
use Feather;
use Vibes\System\GeoIP;
use Vibes\System\BrowserPerformance;
use Vibes\System\WebVitals;
use Vibes\Plugin\Feature\Capture;
/**
* Define the analytics functionality.
*
* Handles all analytics operations.
*
* @package Features
* @author Pierre Lannoy <https://pierre.lannoy.fr/>.
* @since 1.0.0
*/
class Analytics {
/**
* The source of data.
*
* @since 1.0.0
* @var string $source The source of data.
*/
public $source = '';
/**
* The domain name.
*
* @since 1.0.0
* @var string $domain The domain name.
*/
public $domain = '';
/**
* The subdomain name.
*
* @since 1.0.0
* @var string $subdomain The subdomain name.
*/
public $subdomain = '';
/**
* The dashboard type.
*
* @since 1.0.0
* @var string $title The dashboard type.
*/
public $type = '';
/**
* The dashboard extra.
*
* @since 1.0.0
* @var string $extra The dashboard extra.
*/
public $extra = '';
/**
* The queried ID.
*
* @since 1.0.0
* @var string $id The queried ID.
*/
private $id = '';
/**
* The queried site.
*
* @since 1.0.0
* @var string $site The queried site.
*/
public $site = 'all';
/**
* The queried mode.
*
* @since 1.0.0
* @var string $mode The queried mode.
*/
public $mode = 'none';
/**
* The queried country.
*
* @since 1.0.0
* @var string $country The queried country.
*/
public $country = 'all';
/**
* The queried authent.
*
* @since 1.0.0
* @var string $authent The queried authent.
*/
public $authent = 'all';
/**
* The queried channel.
*
* @since 1.1.0
* @var string $channel The queried channel.
*/
public $channel = 'all';
/**
* The start date.
*
* @since 1.0.0
* @var string $start The start date.
*/
private $start = '';
/**
* The end date.
*
* @since 1.0.0
* @var string $end The end date.
*/
private $end = '';
/**
* The period duration in seconds.
*
* @since 1.0.0
* @var integer $duration The period duration in seconds.
*/
private $duration = 0;
/**
* The timezone.
*
* @since 1.0.0
* @var string $timezone The timezone.
*/
private $timezone = 'UTC';
/**
* The main query filter.
*
* @since 1.0.0
* @var array $filter The main query filter.
*/
private $filter = [];
/**
* The query filter for the previous range.
*
* @since 1.0.0
* @var array $previous The query filter fro the previous range.
*/
private $previous = [];
/**
* Is the start date today's date.
*
* @since 1.0.0
* @var boolean $today Is the start date today's date.
*/
private $is_today = false;
/**
* Colors for graphs.
*
* @since 1.0.0
* @var array $colors The colors array.
*/
private $colors = [ '#73879C', '#3398DB', '#9B59B6', '#B2C326', '#FFA5A5', '#A5F8D3', '#FEE440', '#BDC3C6' ];
/**
* Colors for timeline.
*
* @since 1.0.0
* @var array $colors The colors array.
*/
private $span_colors = [
'redirect' => '#3398DB',
'dns' => '#3398DB',
'tcp' => '#3398DB',
'ssl' => '#3398DB',
'wait' => '#9B59B6',
'download' => '#b2c326',
];
/**
* Available countries.
*
* @since 1.0.0
* @var array $available_countries The available countries.
*/
private $available_countries = [];
/**
* The trace ticks.
*
* @since 1.0.0
* @var integer $traces_tick The trace ticks.
*/
private $traces_tick = 100;
/**
* Initialize the class and set its properties.
*
* @param string $source The source of data.
* @param string $domain The domain name, if disambiguation is needed.
* @param string $type The type of analytics ( summary, domain, authority, endpoint, country).
* @param string $site The site to analyze (all or ID).
* @param string $start The start date.
* @param string $end The end date.
* @param string $id The queried ID.
* @param boolean $reload Is it a reload of an already displayed analytics.
* @param string $extra The extra view to render.
* @param string $authent The authent mode.
* @param string $country The country.
* @param string $channel The channel.
* @since 1.0.0
*/
public function __construct( $source, $domain, $type, $site, $start, $end, $id, $reload, $extra, $authent, $country, $channel ) {
$this->source = $source;
$this->id = $id;
$this->extra = $extra;
if ( Role::LOCAL_ADMIN === Role::admin_type() ) {
$site = get_current_blog_id();
}
$this->site = $site;
if ( 'all' !== $site ) {
$this->filter[] = "site='" . $site . "'";
$this->previous[] = "site='" . $site . "'";
}
if ( '' !== $domain ) {
$this->domain = $domain;
$this->filter[] = "id='" . $domain . "'";
$this->previous[] = "id='" . $domain . "'";
}
if ( $start === $end ) {
$this->filter[] = "timestamp='" . $start . "'";
} else {
$this->filter[] = "timestamp>='" . $start . "' and timestamp<='" . $end . "'";
}
$this->start = $start;
$this->end = $end;
$this->type = $type;
if ( 'resource' === $this->source ) {
if ( false !== strpos( $this->type, '~' ) ) {
$ttype = substr( $this->type, 0, strpos( $this->type, '~' ) );
$this->mode = substr( $this->type, strpos( $this->type, '~' ) + 1 );
$this->type = $ttype;
}
if ( 'summary' !== $type ) {
$this->mode = $type;
}
}
if ( '' !== $id ) {
switch ( $type ) {
case 'domain':
case 'authorities':
$this->filter[] = "id='" . $id . "'";
$this->previous[] = "id='" . $id . "'";
break;
case 'authority':
case 'endpoints':
$this->filter[] = "authority='" . $id . "'";
$this->previous[] = "authority='" . $id . "'";
$this->subdomain = Schema::get_authority( $this->source, $this->filter );
break;
case 'endpoint':
$this->filter[] = "endpoint='" . $id . "'";
$this->previous[] = "endpoint='" . $id . "'";
if ( 'resource' === $this->source ) {
$this->subdomain = Schema::get_authority( $this->source, $this->filter );
}
break;
default:
if ( 'webvital' === $this->source ) {
$this->filter[] = "endpoint='" . $id . "'";
$this->previous[] = "endpoint='" . $id . "'";
}
$this->type = 'summary';
}
}
if ( '' !== $domain && 'domain' !== $type && 'authorities' !== $type ) {
$this->domain = $domain;
$this->filter[] = "id='" . $domain . "'";
$this->previous[] = "id='" . $domain . "'";
}
$this->timezone = Timezone::network_get();
$datetime = new \DateTime( 'now', $this->timezone );
$this->is_today = ( $this->start === $datetime->format( 'Y-m-d' ) || $this->end === $datetime->format( 'Y-m-d' ) );
$start = new \DateTime( $this->start, $this->timezone );
$end = new \DateTime( $this->end, $this->timezone );
$start->sub( new \DateInterval( 'P1D' ) );
$end->sub( new \DateInterval( 'P1D' ) );
$delta = $start->diff( $end, true );
if ( $delta ) {
$start->sub( $delta );
$end->sub( $delta );
}
$this->duration = $delta->days + 1;
if ( $start === $end ) {
$this->previous[] = "timestamp='" . $start->format( 'Y-m-d' ) . "'";
} else {
$this->previous[] = "timestamp>='" . $start->format( 'Y-m-d' ) . "' and timestamp<='" . $end->format( 'Y-m-d' ) . "'";
}
if ( 'resource' !== $source ) {
if ( 'all' !== $authent ) {
$this->filter[] = 'authent=' . $authent;
}
$this->authent = $authent;
$this->available_countries = Schema::get_distinct_countries( $this->source, $this->filter, ! $this->is_today );
if ( 'all' !== strtolower( $country ) && ! in_array( strtoupper( $country ), $this->available_countries, true ) ) {
$country = 'all';
}
if ( 'all' !== strtolower( $country ) ) {
$this->filter[] = "country='" . strtoupper( $country ) . "'";
}
$this->country = strtoupper( $country );
if ( 'all' !== strtolower( $channel ) ) {
$url_parts = wp_parse_url( admin_url( '', '' ) );
$cleaned_enpoint = Capture::clean_endpoint( '(self)', $url_parts['path'], 50, false );
switch ( strtolower( $channel ) ) {
case 'wfront':
$this->filter[] = "endpoint not like '" . $cleaned_enpoint . "%'";
break;
case 'wback':
$this->filter[] = "endpoint like'" . $cleaned_enpoint . "%'";
break;
}
}
$this->channel = strtolower( $channel );
}
}
/**
* Get the sampling factor.
*
* @return integer The sampling factor to apply.
* @since 1.0.0
*/
private function sampling_factor() {
if ( 'resource' === $this->source ) {
return (int) ( ( 1000 / Option::network_get( 'sampling' ) ) * ( 1000 / Option::network_get( 'resource_sampling' ) ) );
}
return (int) ( 1000 / Option::network_get( 'sampling' ) );
}
/**
* Query statistics table.
*
* @param string $query The query type.
* @param mixed $queried The query params.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
public function query( $query, $queried ) {
if ( 0 < strpos( $query, '.' ) ) {
$this->source = substr( $query, 0, strpos( $query, '.' ) );
$query = str_replace( $this->source . '.', '', $query );
}
if ( 0 < strpos( $query, '_' ) ) {
$tquery = substr( $query, 0, strpos( $query, '_' ) );
$sub = str_replace( $tquery . '_', '', $query );
$query = $tquery;
} else {
$sub = '';
}
if ( 0 < strpos( $query, '~' ) ) {
$tquery = substr( $query, 0, strpos( $query, '~' ) );
$this->mode = substr( $query, strpos( $query, '~' ) + 1 );
$query = $tquery;
if ( '' !== $this->id ) {
switch ( $this->mode ) {
case 'domain':
case 'authorities':
$this->filter[] = "id='" . $this->id . "'";
$this->previous[] = "id='" . $this->id . "'";
break;
case 'authority':
case 'endpoints':
$this->filter[] = "authority='" . $this->id . "'";
$this->previous[] = "authority='" . $this->id . "'";
$this->subdomain = Schema::get_authority( $this->source, $this->filter );
break;
case 'endpoint':
$this->filter[] = "endpoint='" . $this->id . "'";
$this->previous[] = "endpoint='" . $this->id . "'";
if ( 'resource' === $this->source ) {
$this->subdomain = Schema::get_authority( $this->source, $this->filter );
}
break;
}
}
}
switch ( $query ) {
case 'top-domains':
return $this->query_top( 'domains', (int) $queried );
case 'top-authorities':
return $this->query_top( 'authorities', (int) $queried );
case 'top-endpoints':
return $this->query_top( 'endpoints', (int) $queried );
case 'category':
return $this->query_pie( 'category', (int) $queried );
case 'initiator':
return $this->query_pie( 'initiator', (int) $queried );
case 'security':
return $this->query_pie( 'security', (int) $queried );
case 'cache':
return $this->query_pie( 'cache', (int) $queried );
case 'mimes':
return $this->query_list( 'mimes' );
case 'categories':
return $this->query_list( 'categories' );
case 'domains':
return $this->query_list( 'domains' );
case 'authorities':
return $this->query_list( 'authorities' );
case 'endpoints':
if ( 'webvital' === $this->source ) {
return $this->query_webvital_list( 'endpoints' );
}
return $this->query_navigation_list( 'endpoints' );
case 'sites':
if ( 'webvital' === $this->source ) {
return $this->query_webvital_list( 'sites' );
}
return $this->query_navigation_list( 'sites' );
case 'class':
case 'device':
if ( 'webvital' === $this->source ) {
return $this->query_webvital( $query, $sub );
}
return $this->query_navigation( $query, $sub );
case 'main-webvital-chart':
return $this->query_webvital_chart();
case 'main-navigation-chart':
return $this->query_navigation_chart();
}
return [];
}
/**
* Query statistics table.
*
* @param array $row The type of widget.
* @param string $field The class or device id.
* @return string The result of the query, ready to encode.
* @since 1.0.0
*/
private function get_webvital( $row, $field ) {
if ( 0 === (int) $row[ 'avg_' . $field ] ) {
$value = '-';
$level = 'none';
} else {
$value = WebVitals::display_value( $field, $row[ 'avg_' . $field ] );
$level = WebVitals::get_rate_field( $field, $row[ 'avg_' . $field ] );
}
$result = '<div class="vibes-webvital-container">';
$result .= '<div class="vibes-webvital-text">';
$result .= '<span class="vibes-webvital-definition vibes-webvital-definition-' . $level . '">' . WebVitals::$metrics_names[ $field ] . '</span>';
$result .= '<span class="vibes-webvital-index vibes-webvital-index-' . $level . '">' . $value . '</span>';
$result .= '</div>';
$result .= '<div class="vibes-webvital-bar">';
$result .= '<div class="vibes-webvital-bar-none">' . esc_html__( 'Not Enough Data', 'vibes' ) . '</div>';
$prec = 0;
$shift = 0;
$cpt = 20;
if ( 'none' !== $level ) {
foreach ( [ 'good', 'impr', 'poor' ] as $key => $spec ) {
$remaining = 0;
if ( 'good' === $spec ) {
$remaining = (int) round( 100 * ( $row[ 'pct_' . $field . '_impr' ] + $row[ 'pct_' . $field . '_poor' ] ) );
}
if ( 'impr' === $spec ) {
$remaining = (int) round( 100 * $row[ 'pct_' . $field . '_poor' ] );
}
if ( 0 === $remaining ) {
$val = 100 - $shift;
$pos = 'width:101%;';
} else {
$val = (int) round( 100 * $row[ 'pct_' . $field . '_' . $spec ] );
$pos = 'width:' . ( 1 + 100 * $val / ( 100 - $shift ) ) . '%;';
}
$shift_str = ( 0 === $shift ? '' : 'right:-' . $shift . '%;' );
$up_str = 'top:-' . $cpt . 'px;';
if ( 0 === $prec && 0 !== $val ) {
$width_str = 'width:100%;';
} else {
$width_str = 'width:' . ( 0 === $remaining ? $val + 0.1 : $val ) . '%;';
}
if ( 0 !== $remaining ) {
$width_str = 'width:' . ( 100 - $shift ) . '%;';
}
if ( 0 < $val ) {
$result .= '<div class="vibes-webvital-bar-' . $spec . '" style="' . $width_str . $shift_str . $up_str . '"><span class="vibes-webvital-percent" style="' . $pos . '">' . ( 2 < $val ? $val . '%' : ' ' ) . '</span></div>';
$prec += $val;
$cpt += 20;
} else {
$val = 0;
}
$shift += $val;
}
}
$result .= '</div>';
$result .= '</div>';
return $result;
}
/**
* Query statistics table.
*
* @param string $type The type of widget.
* @param string $id The class or device id.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_webvital( $type, $id ) {
$query = Schema::get_grouped_list( $this->source, $type, [], $this->filter, ! $this->is_today, '', [], false, '', 0, Option::site_get( 'quality' ) );
$data = [];
if ( 0 < count( $query ) ) {
foreach ( $query as $row ) {
if ( $id === $row[ $type ] ) {
$data[] = $row;
}
}
}
if ( 0 === count( $data ) ) {
foreach ( WebVitals::$rated_metrics as $metric ) {
$data[0][ 'avg_' . $metric ] = 0;
foreach ( [ 'good', 'impr', 'poor' ] as $field ) {
$data[0][ 'pct_' . $metric . '_' . $field ] = 0;
}
}
foreach ( WebVitals::$unrated_metrics as $metric ) {
$data[0][ 'avg_' . $metric ] = 0;
}
$data[0]['q'] = 0.0;
} else {
$data[0]['q'] = 0.0;
$idx = Option::network_get( 'qstat' );
foreach ( array_merge( WebVitals::$rated_metrics, WebVitals::$unrated_metrics ) as $metric ) {
$q = $data[0][ 'hit_' . $metric ] / $idx;
if ( 1 < $q ) {
$q = 1.0;
}
$data[0]['q'] += $q * 20;
}
}
$data = $data[0];
$q = (string) round( $data['q'], 5 );
if ( false === strpos( $q, '.' ) ) {
$q .= '.';
$q = str_pad( $q, strlen( $q ) + 1, '0' );
}
$result = '<div class="vibes-webvital-box">';
$result .= '<div class="vibes-corewebvital-box">';
$result .= $this->get_webvital( $data, 'LCP' );
$result .= '<div class="vibes-webvital-separator"> </div>';
$result .= $this->get_webvital( $data, 'FID' );
$result .= '<div class="vibes-webvital-separator"> </div>';
$result .= $this->get_webvital( $data, 'CLS' );
$result .= '<div class="vibes-webvital-separator"> </div>';
$result .= $this->get_webvital( $data, 'FCP' );
$result .= '</div>';
$result .= '<div class="vibes-kpiwebvital-box">';
$result .= '<div class="vibes-kpiwebvital-item">';
$result .= '<div class="vibes-kpiwebvital-title">' . WebVitals::$metrics_names['TTFB'] . '</div>';
$result .= '<div class="vibes-kpiwebvital-value">';
$result .= '<div class="vibes-kpiwebvital-number">' . str_replace( ' ms', '', WebVitals::display_value( 'TTFB', $data['avg_TTFB'] ) ) . '</div>';
$result .= '<div class="vibes-kpiwebvital-unit">ms</div>';
$result .= '</div>';
$result .= '</div>';
$result .= '<div class="vibes-kpiwebvital-item">';
$result .= '<div class="vibes-kpiwebvital-title">' . esc_html__( 'Confidence Index', 'vibes' ) . '</div>';
$result .= '<div class="vibes-kpiwebvital-value">';
$result .= '<div class="vibes-kpiwebvital-number">' . $q . '</div>';
$result .= '<div class="vibes-kpiwebvital-unit">%</div>';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
return [ 'vibes-' . $type . '_' . $id => $result ];
}
/**
* Query statistics table.
*
* @param string $type The type of pie.
* @param integer $limit The number to display.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_pie( $type, $limit ) {
$uuid = UUID::generate_unique_id( 5 );
switch ( $type ) {
case 'category':
$group = 'category';
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, 'ORDER BY sum_hit DESC' );
$size = 200;
break;
case 'initiator':
$group = 'initiator';
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, 'ORDER BY sum_hit DESC' );
$size = 120;
break;
case 'cache':
$group = 'cache';
$data = Schema::get_cache_ratio( $this->source, $this->filter, ! $this->is_today );
$size = 120;
break;
case 'security':
$group = 'scheme';
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, 'ORDER BY sum_hit DESC' );
$size = 120;
break;
}
$total = 0;
$other = 0;
foreach ( $data as $key => $row ) {
$total = $total + $row['sum_hit'];
if ( $limit <= $key ) {
$other = $other + $row['sum_hit'];
}
}
if ( 0 < count( $data ) ) {
$cpt = 0;
$labels = [];
$series = [];
while ( $cpt < $limit && array_key_exists( $cpt, $data ) ) {
if ( 0 < $total ) {
$percent = round( 100 * $data[ $cpt ]['sum_hit'] / $total, 1 );
} else {
$percent = 100;
}
if ( 0.1 > $percent ) {
$percent = 0.1;
}
$meta = strtoupper( $data[ $cpt ][ $group ] );
if ( 'category' === $type ) {
$labels[] = ucwords( strtolower( Mime::get_category_name( $data[ $cpt ][ $group ] ) ) );
} else {
$labels[] = ucwords( strtolower( $data[ $cpt ][ $group ] ) );
}
$series[] = [
'meta' => $meta,
'value' => (float) $percent,
];
++ $cpt;
}
if ( 0 < $other ) {
if ( 0 < $total ) {
$percent = round( 100 * $other / $total, 1 );
} else {
$percent = 100;
}
if ( 0.1 > $percent ) {
$percent = 0.1;
}
$labels[] = esc_html__( 'Other', 'vibes' );
$series[] = [
'meta' => esc_html__( 'Other', 'vibes' ),
'value' => (float) $percent,
];
}
$result = '<div class="vibes-pie-box">';
$result .= '<div class="vibes-pie-graph">';
$result .= '<div class="vibes-pie-graph-handler-' . $size . '" id="vibes-pie-' . $type . '"></div>';
$result .= '</div>';
$result .= '<div class="vibes-pie-legend">';
foreach ( $labels as $key => $label ) {
$icon = '<img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'square', $this->colors[ $key ], $this->colors[ $key ] ) . '" />';
$result .= '<div class="vibes-pie-legend-item">' . $icon . ' ' . $label . '</div>';
}
$result .= '';
$result .= '</div>';
$result .= '</div>';
$result .= '<script>';
$result .= 'jQuery(function ($) {';
$result .= ' var data' . $uuid . ' = ' . wp_json_encode(
[
'labels' => $labels,
'series' => $series,
]
) . ';';
$result .= ' var tooltip' . $uuid . ' = Chartist.plugins.tooltip({percentage: true, appendToBody: true});';
$result .= ' var option' . $uuid . ' = {width: ' . $size . ', height: ' . $size . ', showLabel: false, donut: true, donutWidth: "40%", startAngle: 270, plugins: [tooltip' . $uuid . ']};';
$result .= ' new Chartist.Pie("#vibes-pie-' . $type . '", data' . $uuid . ', option' . $uuid . ');';
$result .= '});';
$result .= '</script>';
} else {
$result = '<div class="vibes-pie-box">';
$result .= '<div class="vibes-pie-graph" style="margin:0 !important;">';
$result .= '<div class="vibes-pie-graph-nodata-handler-' . $size . '" id="vibes-pie-' . $type . '"><span style="position: relative; top: 37px;">- ' . esc_html__( 'No Data', 'vibes' ) . ' -</span></div>';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
}
return [ 'vibes-' . $type => $result ];
}
/**
* Query statistics table.
*
* @param string $type The type of top.
* @param integer $limit The number to display.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_top( $type, $limit ) {
switch ( $type ) {
case 'authorities':
$group = 'authority';
$follow = 'authority';
break;
case 'endpoints':
$group = 'endpoint';
$follow = 'endpoint';
break;
default:
$group = 'id';
$follow = 'domain';
break;
}
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, 'ORDER BY sum_hit DESC' );
$total = 0;
$other = 0;
foreach ( $data as $key => $row ) {
$total = $total + $row['sum_hit'];
if ( $limit <= $key ) {
$other = $other + $row['sum_hit'];
}
}
$factor = $this->sampling_factor();
$result = '';
$cpt = 0;
while ( $cpt < $limit && array_key_exists( $cpt, $data ) ) {
if ( 0 < $total ) {
$percent = round( 100 * $data[ $cpt ]['sum_hit'] / $total, 1 );
} else {
$percent = 100;
}
$url = $this->get_url(
[],
[
'type' => $follow,
'id' => $data[ $cpt ][ $group ],
'domain' => $data[ $cpt ]['id'],
]
);
if ( 0.5 > $percent ) {
$percent = 0.5;
}
$text = $data[ $cpt ][ $group ];
if ( 60 < strlen( $text ) ) {
$text = substr( $text, 0, 60 ) . '…';
}
$result .= '<div class="vibes-top-line">';
$result .= '<div class="vibes-top-line-title">';
$result .= '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $data[ $cpt ]['id'] ) . '" /> <span class="vibes-top-line-title-text"><a href="' . esc_url( $url ) . '">' . $text . '</a></span>';
$result .= '</div>';
$result .= '<div class="vibes-top-line-content">';
$result .= '<div class="vibes-bar-graph"><div class="vibes-bar-graph-value" style="width:' . $percent . '%"></div></div>';
$result .= '<div class="vibes-bar-detail">' . Conversion::number_shorten( $data[ $cpt ]['sum_hit'] * $factor, 2, false, ' ' ) . '</div>';
$result .= '</div>';
$result .= '</div>';
++$cpt;
}
if ( 0 < $total ) {
$percent = round( 100 * $other / $total, 1 );
} else {
$percent = 100;
}
$result .= '<div class="vibes-top-line vibes-minor-data">';
$result .= '<div class="vibes-top-line-title">';
$result .= '<span class="vibes-top-line-title-text">' . esc_html__( 'Other', 'vibes' ) . '</span>';
$result .= '</div>';
$result .= '<div class="vibes-top-line-content">';
$result .= '<div class="vibes-bar-graph"><div class="vibes-bar-graph-value" style="width:' . $percent . '%"></div></div>';
$result .= '<div class="vibes-bar-detail">' . Conversion::number_shorten( $other * $factor, 2, false, ' ' ) . '</div>';
$result .= '</div>';
$result .= '</div>';
return [ 'vibes-top-' . $type => $result ];
}
/**
* Print the single span name.
*
* @since 1.0.0
*/
private function get_span_name( $name ) {
switch ( $name ) {
case 'redirect':
return __( 'Redirections', 'vibes' );
case 'dns':
return __( 'DNS Lookup', 'vibes' );
case 'tcp':
return __( 'TCP Connection', 'vibes' );
case 'ssl':
return __( 'SSL Handshake', 'vibes' );
case 'wait':
return __( 'Waiting', 'vibes' );
case 'download':
return __( 'Download', 'vibes' );
}
}
/**
* Print the single span visualization.
*
* @since 1.0.0
*/
private function get_span( $span, $level = 0, $start = 0, $duration = 0 ) {
$time = (string) round( ( $span['duration'] ), 1 );
if ( false === strpos( $time, '.' ) ) {
$time .= '.0';
}
$time .= ' ms';
$result = '<div class="vibes-span-wrap">';
// Span text
$result .= '<div class="vibes-span-text">';
$result .= '<span class="vibes-span-text-label">' . str_pad( '', ( $level * 3 ) * 6, ' ' ) . ( 0 === $level ? '' : ' ' ) . $this->get_span_name( $span['name'] ) . $span['comp'] . '</span>';
$result .= '<span class="vibes-span-text-time">' . $time . '</span>';
$result .= '</div>';
// Span timeline
$bblank = round( 100 * ( $span['start'] - $start ) / $duration, 2 );
$lblank = round( 100 * ( $span['duration'] ) / $duration, 2 );
if ( 0.1 > $lblank ) {
$lblank = 0.1;
}
$eblank = 100.0 - $bblank - $lblank;
$tick = round( 200 * $this->traces_tick / $duration, 3 );
$color = $this->span_colors[ $span['name'] ];
if ( 0 > $eblank ) {
$eblank = 0.0;
$bblank = 100.0 - $lblank;
}
if ( 90 > $bblank ) {
$result .= '<div class="vibes-span-timeline" style="background-size: ' . $tick . '% 100%;">';
$result .= '<div class="vibes-span-timeline-blank" style="width:' . $bblank . '%">';
$result .= '</div>';
$result .= '<div class="vibes-span-timeline-line" style="background-color:' . $color . ';width:' . $lblank . '%">';
$result .= '</div>';
$result .= '<div class="vibes-span-timeline-blank" style="width:' . $eblank . '%;">';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
} else {
$result .= '<div class="vibes-span-timeline" style="background-size: ' . $tick . '% 100%;">';
$result .= '<div class="vibes-span-timeline-blank" style="width:' . $bblank . '%;vertical-align: middle;">';
$result .= '</div>';
$result .= '<div class="vibes-span-timeline-line" style="background-color:' . $color . ';width:' . $lblank . '%">';
$result .= '</div>';
$result .= '<div class="vibes-span-timeline-blank" style="width:' . $eblank . '%;">';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
}
return $result;
}
/**
* Get a trace visualization.
*
* @param array $row The type of list.
* @return string The span, ready to print.
* @since 1.0.0
*/
private function get_spans( $row ) {
$result = '<div class="vibes-spans-wrap" style="width:100%">';
$spans = [];
$duration = 0;
foreach ( BrowserPerformance::$spans as $span ) {
if ( array_key_exists( 'avg_span_' . $span . '_start', $row ) && array_key_exists( 'avg_span_' . $span . '_duration', $row ) ) {
$s = [];
if ( 'redirect' === $span && array_key_exists( 'avg_redirects', $row ) ) {
$s['comp'] = ' (' . (int) round( $row['avg_redirects'], 0 ) . ')';
} else {
$s['comp'] = '';
}
$s['name'] = $span;
$s['start'] = $row[ 'avg_span_' . $span . '_start' ];
$s['duration'] = $row[ 'avg_span_' . $span . '_duration' ];
$spans[] = $s;
}
}
if ( array_key_exists( 'avg_span_download_start', $row ) && array_key_exists( 'avg_span_download_duration', $row ) ) {
$duration = $row['avg_span_download_start'] + $row['avg_span_download_duration'] + 1;
}
$duration += (int) ( $this->traces_tick / 10 );
if ( 3 * $this->traces_tick > $duration ) {
$duration = 3 * $this->traces_tick;
}
foreach ( $spans as $span ) {
$result .= $this->get_span( $span, 0, 0, $duration );
}
$result .= '</div>';
return $result;
}
/**
* Query statistics table.
*
* @param string $type The type of widget.
* @param string $id The class or device id.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_navigation( $type, $id ) {
$query = Schema::get_grouped_list( $this->source, $type, [], $this->filter, ! $this->is_today, '', [], false, '', 0, Option::site_get( 'quality' ) );
$data = [];
if ( 0 < count( $query ) ) {
foreach ( $query as $row ) {
if ( $id === $row[ $type ] ) {
$data[] = $row;
}
}
}
if ( 0 !== count( $data ) ) {
$result = '<div class="vibes-span-widget">' . $this->get_spans( $data[0] ) . '</div>';
} else {
$result = '<div class="vibes-pie-box">';
$result .= '<div class="vibes-pie-graph" style="margin:0 !important;">';
$result .= '<div class="vibes-pie-graph-nodata-handler-200" id="vibes-pie-' . $type . '"><span style="position: relative; top: 37px;">- ' . esc_html__( 'Not Enough Data', 'vibes' ) . ' -</span></div>';
$result .= '</div>';
$result .= '</div>';
$result .= '</div>';
}
return [ 'vibes-' . $type . '_' . $id => $result ];
}
/**
* Query statistics table.
*
* @param string $type The type of list.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_list( $type ) {
$follow = '';
$has_detail = false;
$detail = '';
$count = [];
$toggle = false;
switch ( $type ) {
case 'mimes':
$group = 'category, mime';
$toggle = true;
break;
case 'categories':
$group = 'category';
$toggle = true;
break;
case 'domains':
$group = 'id';
$follow = 'domain';
$count = [ 'authority', 'endpoint' ];
$has_detail = true;
break;
case 'authorities':
$group = 'authority';
$follow = 'authority';
$count = [ 'endpoint' ];
$has_detail = true;
break;
case 'endpoints':
$group = 'endpoint';
$follow = 'endpoint';
$count = [ 'mime' ];
$has_detail = true;
break;
case 'sites':
$group = 'site';
break;
}
$data = Schema::get_grouped_list( $this->source, $group, $count, $this->filter, ! $this->is_today, '', [], false, 'ORDER BY sum_hit DESC' );
$factor = $this->sampling_factor();
$detail_name = esc_html__( 'Details', 'vibes' );
$calls_name = esc_html__( 'Hits', 'vibes' );
$data_name = esc_html__( 'Size', 'vibes' );
$latency_name = esc_html__( 'Latency', 'vibes' );
$cache_name = esc_html__( 'Browser Cache', 'vibes' );
$result = '<table class="vibes-table">';
$result .= '<tr>';
$result .= '<th> </th>';
if ( $has_detail ) {
$result .= '<th>' . $detail_name . '</th>';
}
$result .= '<th>' . $calls_name . '</th>';
$result .= '<th>' . $data_name . '</th>';
$result .= '<th>' . $latency_name . '</th>';
$result .= '<th>' . $cache_name . '</th>';
$result .= '</tr>';
$other = false;
$other_str = '';
$geoip = new GeoIP();
foreach ( $data as $key => $row ) {
$url = $this->get_url(
[],
[
'type' => $follow,
'id' => false === strpos( $group, ',' ) ? $row[ $group ] : 'unknown',
'domain' => $row['id'],
]
);
switch ( $type ) {
case 'mimes':
$name = '<details class="vibes-span-details vibes-span-' . $type . '"><summary class="vibes-span-summary"><img style="width:14px;vertical-align:text-bottom;" src="' . Mime::get_category_icon( $row['category'] ) . '" /> <span class="vibes-table-text">' . $row['mime'] . '</span></summary><div class="vibes-span-container">' . $this->get_spans( $row ) . '</div></details>';
break;
case 'categories':
$name = '<details class="vibes-span-details vibes-span-' . $type . '"><summary class="vibes-span-summary"><img style="width:14px;vertical-align:text-bottom;" src="' . Mime::get_category_icon( $row['category'] ) . '" /> <span class="vibes-table-text">' . Mime::get_category_name( $row['category'] ) . '</span></summary><div class="vibes-span-container">' . $this->get_spans( $row ) . '</div></details>';
break;
case 'domains':
$authorities = sprintf( esc_html( _n( '%d subdomain', '%d subdomains', $row['cnt_authority'], 'vibes' ) ), $row['cnt_authority'] );
$endpoints = sprintf( esc_html( _n( '%d endpoint', '%d endpoints', $row['cnt_endpoint'], 'vibes' ) ), $row['cnt_endpoint'] );
$detail = $authorities . ' - ' . $endpoints;
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $row['id'] ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row[ $group ] . '</a></span>';
break;
case 'authorities':
$detail = sprintf( esc_html( _n( '%d endpoint', '%d endpoints', $row['cnt_endpoint'], 'vibes' ) ), $row['cnt_endpoint'] );
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $row['id'] ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row[ $group ] . '</a></span>';
break;
case 'endpoints':
if ( 1 === (int) $row['cnt_mime'] ) {
$detail = $row['mime'];
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Mime::get_category_icon( $row['category'] ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row[ $group ] . '</a></span>';
} else {
$detail = sprintf( esc_html( _n( '%d mime type', '%d different mime types', $row['cnt_mime'], 'vibes' ) ), $row['cnt_mime'] );
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $row['id'] ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row[ $group ] . '</a></span>';
}
break;
case 'sites':
if ( 0 === (int) $row['sum_hit'] ) {
break;
}
if ( 'summary' === $this->type ) {
$url = $this->get_url(
[],
[
'site' => $row['site'],
]
);
} else {
$url = $this->get_url(
[],
[
'site' => $row['site'],
'domain' => $row['id'],
]
);
}
$site = Blog::get_blog_url( $row['site'] );
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $site ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $site . '</a></span>';
break;
}
$calls = Conversion::number_shorten( $row['sum_hit'] * $factor, 2, false, ' ' );
if ( 0 < $row['avg_size'] ) {
$data = Conversion::data_shorten( (int) $row['avg_size'], 2, false, ' ' );
} else {
$data = '-';
}
$cache = round( 100 * $row['avg_cache'], 1 ) . '%';
$latency = (int) $row['avg_load'] . ' ' . esc_html_x( 'ms', 'Unit symbol - Stands for "millisecond".', 'vibes' );
if ( 'codes' === $type && '0' === $row[ $group ] ) {
$latency = '-';
}
$row_str = '<tr>';
$row_str .= '<td data-th="">' . $name . '</td>';
if ( $has_detail ) {
$row_str .= '<td data-th="' . $detail_name . '">' . $detail . '</td>';
}
$row_str .= '<td data-th="' . $calls_name . '">' . $calls . '</td>';
$row_str .= '<td data-th="' . $data_name . '">' . $data . '</td>';
$row_str .= '<td data-th="' . $latency_name . '">' . $latency . '</td>';
$row_str .= '<td data-th="' . $cache_name . '">' . $cache . '</td>';
$row_str .= '</tr>';
if ( $other ) {
$other_str = $row_str;
} else {
$result .= $row_str;
}
}
$result .= $other_str . '</table>';
if ( $toggle ) {
$result .= '<script>jQuery(document).ready(function($){$(".vibes-span-' . $type . '").on("toggle",function(e){if(this.open){$(".vibes-span-' . $type . '").not(this).attr({open:false});}})});</script>';
}
return [ 'vibes-' . $type => $result ];
}
/**
* Query statistics table.
*
* @param string $type The type of list.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_webvital_list( $type ) {
$follow = '';
$order = '';
switch ( $type ) {
case 'endpoints':
$group = 'endpoint';
$follow = 'endpoint';
$order = 'ORDER BY endpoint ASC';
break;
case 'sites':
$group = 'site';
$order = 'ORDER BY site DESC';
break;
}
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, $order );
$result = '<table class="vibes-table">';
$result .= '<tr>';
$result .= '<th> </th>';
foreach ( array_merge( WebVitals::$rated_metrics, WebVitals::$unrated_metrics ) as $metric ) {
$result .= '<th>' . $metric . '</th>';
}
$result .= '</tr>';
$site = '';
$icon = '';
foreach ( $data as $key => $row ) {
$url = $this->get_url(
[],
[
'type' => $follow,
'id' => false === strpos( $group, ',' ) ? $row[ $group ] : 'unknown',
]
);
switch ( $type ) {
case 'endpoints':
if ( '' === $site ) {
$site = Blog::get_blog_url( $row['site'] );
$icon = Favicon::get_base64( $site );
}
$name = '<img style="width:16px;vertical-align:bottom;" src="' . $icon . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row['endpoint'] . '</a></span>';
break;
case 'sites':
$url = $this->get_url(
[],
[
'site' => $row['site'],
]
);
$site = Blog::get_blog_url( $row['site'] );
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $site ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $site . '</a></span>';
break;
}
$result .= '<tr>';
$result .= '<td data-th="">' . $name . '</td>';
foreach ( array_merge( WebVitals::$rated_metrics, WebVitals::$unrated_metrics ) as $metric ) {
$result .= '<td data-th="' . $metric . '"><span class="vibes-list-item-' . WebVitals::get_rate_field( $metric, $row[ 'avg_' . $metric ] ) . '">' . WebVitals::display_value( $metric, $row[ 'avg_' . $metric ] ) . '</span></td>';
}
$result .= '</tr>';
}
$result .= '</table>';
return [ 'vibes-' . $type => $result ];
}
/**
* Query statistics table.
*
* @param string $type The type of list.
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_navigation_list( $type ) {
$follow = '';
$order = '';
switch ( $type ) {
case 'endpoints':
$group = 'endpoint';
$follow = 'endpoint';
$order = 'ORDER BY endpoint ASC';
break;
case 'sites':
$group = 'site';
$order = 'ORDER BY site DESC';
break;
}
$data = Schema::get_grouped_list( $this->source, $group, [], $this->filter, ! $this->is_today, '', [], false, $order );
$factor = $this->sampling_factor();
$calls_name = esc_html__( 'Hits', 'vibes' );
$redirect_name = esc_html__( 'Redirects', 'vibes' );
$data_name = esc_html__( 'Size', 'vibes' );
$latency_name = esc_html__( 'Latency', 'vibes' );
$cache_name = esc_html__( 'Browser Cache', 'vibes' );
$result = '<table class="vibes-table">';
$result .= '<tr>';
$result .= '<th> </th>';
$result .= '<th>' . $calls_name . '</th>';
$result .= '<th>' . $redirect_name . '</th>';
$result .= '<th>' . $data_name . '</th>';
$result .= '<th>' . $latency_name . '</th>';
$result .= '<th>' . $cache_name . '</th>';
$result .= '</tr>';
$site = '';
$icon = '';
foreach ( $data as $key => $row ) {
$url = $this->get_url(
[],
[
'type' => $follow,
'id' => false === strpos( $group, ',' ) ? $row[ $group ] : 'unknown',
]
);
switch ( $type ) {
case 'endpoints':
if ( '' === $site ) {
$site = Blog::get_blog_url( $row['site'] );
$icon = Favicon::get_base64( $site );
}
$name = '<img style="width:16px;vertical-align:bottom;" src="' . $icon . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $row['endpoint'] . '</a></span>';
break;
case 'sites':
$url = $this->get_url(
[],
[
'site' => $row['site'],
]
);
$site = Blog::get_blog_url( $row['site'] );
$name = '<img style="width:16px;vertical-align:bottom;" src="' . Favicon::get_base64( $site ) . '" /> <span class="vibes-table-text"><a href="' . esc_url( $url ) . '">' . $site . '</a></span>';
break;
}
$calls = Conversion::number_shorten( $row['sum_hit'] * $factor, 2, false, ' ' );
$redir = (int) $row['avg_redirects'];
if ( 0 < $row['avg_size'] ) {
$data = Conversion::data_shorten( (int) $row['avg_size'], 2, false, ' ' );
} else {
$data = '-';
}
$cache = round( 100 * $row['avg_cache'], 1 ) . '%';
$latency = (int) $row['avg_load'] . ' ' . esc_html_x( 'ms', 'Unit symbol - Stands for "millisecond".', 'vibes' );
$result .= '<tr>';
$result .= '<td data-th="">' . $name . '</td>';
$result .= '<td data-th="' . $calls_name . '">' . $calls . '</td>';
$result .= '<td data-th="' . $redirect_name . '">' . $redir . '</td>';
$result .= '<td data-th="' . $data_name . '">' . $data . '</td>';
$result .= '<td data-th="' . $latency_name . '">' . $latency . '</td>';
$result .= '<td data-th="' . $cache_name . '">' . $cache . '</td>';
$result .= '</tr>';
}
$result .= '</table>';
return [ 'vibes-' . $type => $result ];
}
/**
* Query statistics table.
*
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_webvital_chart() {
$data = Schema::get_time_series( 'webvital', $this->filter, ! $this->is_today );
if ( 0 === count( $data ) ) {
return [ 'vibes-webvital-chart' => '<div class="vibes-multichart-handler"><div class="vibes-multichart-nodata-handler"><span style="position: relative; top: 37px;">- ' . esc_html__( 'Not Enough Data', 'vibes' ) . ' -</span></div></div>' ];
}
$uuid = UUID::generate_unique_id( 5 );
$series = [];
$metrics = array_merge( WebVitals::$rated_metrics, WebVitals::$unrated_metrics );
$start = '';
$max = [];
$init = strtotime( $this->start ) - 86400;
for ( $i = 0; $i < $this->duration + 2; $i++ ) {
$ts = 'new Date(' . (string) ( ( $i * 86400 ) + $init ) . '000)';
foreach ( $metrics as $metric ) {
$series[ strtolower( $metric ) ]['avg'][] = [
'x' => $ts,
'y' => 'null',
];
foreach ( [ 'good', 'impr', 'poor' ] as $field ) {
$series[ strtolower( $metric ) ][ $field ][] = [
'x' => $ts,
'y' => 'null',
];
}
}
}
foreach ( $data as $timestamp => $row ) {
if ( '' === $start ) {
$start = $timestamp;
}
$ts = 'new Date(' . (string) strtotime( $timestamp ) . '000)';
$idx = (int) ( ( strtotime( $timestamp ) - $init ) / 86400 );
foreach ( $metrics as $metric ) {
$val = WebVitals::get_graphable_value( $metric, $row[ 'avg_' . $metric ] );
if ( ( array_key_exists( strtolower( $metric ), $max ) && $max[ strtolower( $metric ) ] < $val ) || ! array_key_exists( strtolower( $metric ), $max ) ) {
$max[ strtolower( $metric ) ] = $val;
}
$series[ strtolower( $metric ) ]['avg'][$idx] = [
'x' => $ts,
'y' => $val,
];
foreach ( [ 'good', 'impr', 'poor' ] as $field ) {
if ( array_key_exists( 'pct_' . $metric . '_' . $field, $row ) ) {
$series[ strtolower( $metric ) ][ $field ][$idx] = [
'x' => $ts,
'y' => round( 100 * $row[ 'pct_' . $metric . '_' . $field ], 1 ),
];
}
}
}
}
$scale = [];
foreach ( $metrics as $metric ) {
$metric = strtolower( $metric );
$series[ $metric ]['avg'] = wp_json_encode(
[
'series' => [
[
'name' => strtoupper( $metric ),
'data' => $series[ $metric ]['avg'],
],
],
],
);
$series[ $metric ]['avg'] = str_replace( '"x":"new', '"x":new', $series[ $metric ]['avg'] );
$series[ $metric ]['avg'] = str_replace( ')","y"', '),"y"', $series[ $metric ]['avg'] );
$series[ $metric ]['avg'] = str_replace( '"null"', 'null', $series[ $metric ]['avg'] );
if ( 'ttfb' !== $metric ) {
$series[ $metric ]['pct'] = wp_json_encode(
[
'series' => [
[
'name' => esc_html__( 'Good', 'vibes' ),
'data' => $series[ $metric ]['good'],
],
[
'name' => esc_html__( 'Needs Improvement', 'vibes' ),
'data' => $series[ $metric ]['impr'],
],
[
'name' => esc_html__( 'Poor', 'vibes' ),
'data' => $series[ $metric ]['poor'],
],
],
],
);
} else {
$series[ $metric ]['pct'] = wp_json_encode(
[
'series' => [],
],
);
}
$series[ $metric ]['pct'] = str_replace( '"x":"new', '"x":new', $series[ $metric ]['pct'] );
$series[ $metric ]['pct'] = str_replace( ')","y"', '),"y"', $series[ $metric ]['pct'] );
$series[ $metric ]['pct'] = str_replace( '"null"', 'null', $series[ $metric ]['pct'] );
if ( 'ttfb' === $metric ) {
$scale[ $metric ] = [ 500, 1000, 1500, 2000, 2500 ];
$scale[ $metric ] = 'ticks: [' . implode( ',', $scale[ $metric ] ) . '], ';
$max[ $metric ] = '';
} else {
foreach ( WebVitals::$metrics_rates[ strtoupper( $metric ) ] as $rate ) {
$val = ( $rate / WebVitals::$metrics_ratios[ strtoupper( $metric ) ] );
if ( $max[ $metric ] < $val ) {
$max[ $metric ] = $val;
}
$scale[ $metric ][] = $val;
}
$scale[ $metric ] = 'ticks: [' . implode( ',', $scale[ $metric ] ) . '], ';
$max[ $metric ] = 'high: ' . $max[ $metric ] . ', ';
}
}
// Rendering.
$ticks = (int) ( 1 + ( $this->duration / 15 ) );
$result = '<div class="vibes-multichart-handler">';
foreach ( $metrics as $metric ) {
$metric = strtolower( $metric );
$result .= '<div class="vibes-multichart-item' . ( 'lcp' === $metric ? ' active' : '' ) . '" id="vibes-chart-' . $metric . '">';
$result .= '<div class="vibes-multichart-line-container" id="vibes-line-' . $metric . '"></div>';
$result .= '<div class="vibes-multichart-bars-container" id="vibes-bars-' . $metric . '"></div>';
$result .= '</div>';
$result .= '<script>';
$result .= 'jQuery(function ($) {';
$result .= ' var ' . $metric . '_line_data' . $uuid . ' = ' . $series[ $metric ]['avg'] . ';';
$result .= ' var ' . $metric . '_bars_data' . $uuid . ' = ' . $series[ $metric ]['pct'] . ';';
$result .= ' var ' . $metric . '_line_tooltip' . $uuid . ' = Chartist.plugins.tooltip({percentage: false, appendToBody: true});';
$result .= ' var ' . $metric . '_bars_tooltip' . $uuid . ' = Chartist.plugins.tooltip({percentage: true, appendToBody: true});';
$result .= ' var ' . $metric . '_line_option' . $uuid . ' = {';
$result .= ' height: 300,';
$result .= ' fullWidth: true,';
$result .= ' showArea: false,';
$result .= ' showLine: true,';
$result .= ' showPoint: false,';
$result .= ' plugins: [' . $metric . '_line_tooltip' . $uuid . '],';
$result .= ' axisX: {scaleMinSpace: 10, type: Chartist.FixedScaleAxis, divisor:' . ( $this->duration + 1 ) . ', labelInterpolationFnc: function skipLabels(value, index, labels) {return 0 === index % ' . $ticks . ' ? moment(value).format("DD") : null;}},';
$result .= ' axisY: {type: Chartist.FixedScaleAxis, ' . $scale[ $metric ] . $max[ $metric ] . 'low: 0, labelInterpolationFnc: function (value) {return value.toString()' . ( 'cls' === $metric ? '' : ' + " ms"' ) . ';}},';
$result .= ' };';
$result .= ' var ' . $metric . '_bars_option' . $uuid . ' = {';
$result .= ' height: 300,';
$result .= ' stackBars: true,';
$result .= ' stackMode: "accumulate",';
$result .= ' seriesBarDistance: 0,high: 100, low: 0,';
$result .= ' plugins: [' . $metric . '_bars_tooltip' . $uuid . '],';
$result .= ' axisX: {scaleMinSpace: 10, type: Chartist.FixedScaleAxis, divisor:' . ( $this->duration + 1 ) . '},';
$result .= ' };';
$result .= ' new Chartist.Line("#vibes-line-' . $metric . '", ' . $metric . '_line_data' . $uuid . ', ' . $metric . '_line_option' . $uuid . ');';
$result .= ' new Chartist.Bar("#vibes-bars-' . $metric . '", ' . $metric . '_bars_data' . $uuid . ', ' . $metric . '_bars_option' . $uuid . ');';
$result .= '});';
$result .= '</script>';
}
$result .= '</div>';
return [ 'vibes-webvital-chart' => $result ];
}
/**
* Query statistics table.
*
* @return array The result of the query, ready to encode.
* @since 1.0.0
*/
private function query_navigation_chart() {
$data = Schema::get_time_series( 'navigation', $this->filter, ! $this->is_today );
if ( 0 === count( $data ) ) {
return [ 'vibes-navigation-chart' => '<div class="vibes-multichart-handler"><div class="vibes-multichart-nodata-handler"><span style="position: relative; top: 37px;">- ' . esc_html__( 'Not Enough Data', 'vibes' ) . ' -</span></div></div>' ];
}
$uuid = UUID::generate_unique_id( 5 );
$series = [];
$metrics = [];
$metrics[] = [ 'avg_load', 'avg_span_wait_duration', 'avg_span_download_duration' ];
$metrics[] = [ 'avg_span_redirect_duration', 'avg_span_dns_duration', 'avg_span_tcp_duration' ];
$start = '';
$max = [];
$max[0] = [];
$max[1] = [];
$init = strtotime( $this->start ) - 86400;
for ( $i = 0; $i < $this->duration + 2; $i++ ) {
$ts = 'new Date(' . (string) ( ( $i * 86400 ) + $init ) . '000)';
foreach ( $metrics as $stack ) {
foreach ( $stack as $metric ) {
$series[ $metric ]['avg'][] = [
'x' => $ts,
'y' => 'null',
];
}
}
}
foreach ( $data as $timestamp => $row ) {
if ( '' === $start ) {
$start = $timestamp;
}
$ts = 'new Date(' . (string) strtotime( $timestamp ) . '000)';
$idx = (int) ( ( strtotime( $timestamp ) - $init ) / 86400 );
foreach ( $metrics as $key => $stack ) {
foreach ( $stack as $metric ) {
$val = $row[ $metric ];
if ( ( array_key_exists( $metric, $max[ $key ] ) && $max[ $key ][ $metric ] < $val ) || ! array_key_exists( $metric, $max[ $key ] ) ) {
$max[ $key ][ $metric ] = $val;
}
$series[ $metric ]['avg'][$idx] = [
'x' => $ts,
'y' => $val,
];
}
}
}
$series['stack0']['avg'] = wp_json_encode(
[
'series' => [
[
'name' => esc_html__( 'Latency', 'vibes' ),
'data' => $series['avg_load']['avg'],
],
[
'name' => esc_html__( 'Waiting', 'vibes' ),
'data' => $series['avg_span_wait_duration']['avg'],
],
[
'name' => esc_html__( 'Download', 'vibes' ),
'data' => $series['avg_span_download_duration']['avg'],
],
],
],
);
$series['stack1']['avg'] = wp_json_encode(
[
'series' => [
[
'name' => esc_html__( 'Redirections', 'vibes' ),
'data' => $series['avg_span_redirect_duration']['avg'],
],
[
'name' => esc_html__( 'DNS Lookup', 'vibes' ),
'data' => $series['avg_span_dns_duration']['avg'],
],
[
'name' => esc_html__( 'Connection', 'vibes' ),
'data' => $series['avg_span_redirect_duration']['avg'],
],
],
],
);
foreach ( $metrics as $key => $stack ) {
$series[ 'stack' . $key ]['avg'] = str_replace( '"x":"new', '"x":new', $series[ 'stack' . $key ]['avg'] );
$series[ 'stack' . $key ]['avg'] = str_replace( ')","y"', '),"y"', $series[ 'stack' . $key ]['avg'] );
$series[ 'stack' . $key ]['avg'] = str_replace( '"null"', 'null', $series[ 'stack' . $key ]['avg'] );
}
// Rendering.
$ticks = (int) ( 1 + ( $this->duration / 15 ) );
$result = '<div class="vibes-multichart-handler">';
$result .= '<div class="vibes-multichart-item active" id="vibes-chart-time">';
$result .= '</div>';
$result .= '<script>';
$result .= 'jQuery(function ($) {';
$result .= ' var time_data' . $uuid . ' = ' . $series['stack0']['avg'] . ';';
$result .= ' var time_tooltip' . $uuid . ' = Chartist.plugins.tooltip({percentage: false, appendToBody: true});';
$result .= ' var time_option' . $uuid . ' = {';
$result .= ' height: 300,';
$result .= ' fullWidth: true,';
$result .= ' showArea: true,';
$result .= ' showLine: true,';
$result .= ' showPoint: false,';
$result .= ' plugins: [time_tooltip' . $uuid . '],';
//$result .= ' axisX: {scaleMinSpace: 100, type: Chartist.FixedScaleAxis, divisor:' . $divisor . ', labelInterpolationFnc: function (value) {return moment(value).format("YYYY-MM-DD");}},';
$result .= ' axisX: {scaleMinSpace: 10, type: Chartist.FixedScaleAxis, divisor:' . ( $this->duration + 1 ) . ', labelInterpolationFnc: function skipLabels(value, index, labels) {return 0 === index % ' . $ticks . ' ? moment(value).format("DD") : null;}},';
$result .= ' axisY: {type: Chartist.FixedScaleAxis, ticks: [1000, 2000, 3000],low: 0, labelInterpolationFnc: function (value) {return value.toString() + " ms";}},';
$result .= ' };';
$result .= ' new Chartist.Line("#vibes-chart-time", time_data' . $uuid . ', time_option' . $uuid . ');';
$result .= '});';
$result .= '</script>';
$result .= '<div class="vibes-multichart-item" id="vibes-chart-net">';
$result .= '</div>';
$result .= '<script>';
$result .= 'jQuery(function ($) {';
$result .= ' var net_net' . $uuid . ' = ' . $series['stack1']['avg'] . ';';
$result .= ' var net_tooltip' . $uuid . ' = Chartist.plugins.tooltip({percentage: false, appendToBody: true});';
$result .= ' var net_option' . $uuid . ' = {';
$result .= ' height: 300,';
$result .= ' fullWidth: true,';
$result .= ' showArea: true,';
$result .= ' showLine: true,';
$result .= ' showPoint: false,';
$result .= ' plugins: [net_tooltip' . $uuid . '],';
//$result .= ' axisX: {type: Chartist.FixedScaleAxis, divisor:' . $divisor . ', labelInterpolationFnc: function (value) {return moment(value).format("YYYY-MM-DD");}},';
$result .= ' axisX: {scaleMinSpace: 10, type: Chartist.FixedScaleAxis, divisor:' . ( $this->duration + 1 ) . ', labelInterpolationFnc: function skipLabels(value, index, labels) {return 0 === index % ' . $ticks . ' ? moment(value).format("DD") : null;}},';
$result .= ' axisY: {type: Chartist.AutoScaleAxis, labelInterpolationFnc: function (value) {return value.toString() + " ms";}},';
$result .= ' };';
$result .= ' new Chartist.Line("#vibes-chart-net", net_net' . $uuid . ', net_option' . $uuid . ');';
$result .= '});';
$result .= '</script>';
$result .= '</div>';
return [ 'vibes-navigation-chart' => $result ];
}
/**
* Get the channel selector.
*
* @return string The selector ready to print.
* @since 1.1.0
*/
public function get_channel_selector() {
$breadcrumbs[] = [
'title' => ' - ' . esc_html__( 'All', 'vibes' ) . ' - ',
'url' => $this->get_url( [ 'channel' ] ),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Frontend', 'vibes' ),
'url' => $this->get_url( [], [ 'channel' => 'wfront' ] ),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Backend', 'vibes' ),
'url' => $this->get_url( [], [ 'channel' => 'wback' ] ),
];
if ( 'all' === $this->channel ) {
$title = esc_html__( 'All', 'vibes' );
} elseif ( 'wfront' === $this->channel ) {
$title = esc_html__( 'Frontend', 'vibes' );
} elseif ( 'wback' === $this->channel ) {
$title = esc_html__( 'Backend', 'vibes' );
}
$result = '<select name="channel" id="channel" data="' . Feather\Icons::get_base64( 'activity', 'none', '#5A738E' ) . '" class="vibes-select channel" placeholder="' . $title . '" style="display:none;">';
foreach ( $breadcrumbs as $breadcrumb ) {
$result .= '<option value="' . $breadcrumb['url'] . '">' . $breadcrumb['title'] . '</option>';
}
$result .= '</select>';
$result .= '';
return $result;
}
/**
* Get the user selector.
*
* @return string The selector ready to print.
* @since 1.0.0
*/
public function get_user_selector() {
$breadcrumbs[] = [
'title' => ' - ' . esc_html__( 'All', 'vibes' ) . ' - ',
'url' => $this->get_url( [ 'authent' ] ),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Anonymous', 'vibes' ),
'url' => $this->get_url( [], [ 'authent' => 0 ] ),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Authenticated', 'vibes' ),
'url' => $this->get_url( [], [ 'authent' => 1 ] ),
];
if ( 'all' === $this->authent ) {
$title = esc_html__( 'All', 'vibes' );
} elseif ( 0 === (int) $this->authent ) {
$title = esc_html__( 'Anonymous', 'vibes' );
} elseif ( 1 === (int) $this->authent ) {
$title = esc_html__( 'Authenticated', 'vibes' );
}
$result = '<select name="users" id="users" data="' . Feather\Icons::get_base64( 'users', 'none', '#5A738E' ) . '" class="vibes-select users" placeholder="' . $title . '" style="display:none;">';
foreach ( $breadcrumbs as $breadcrumb ) {
$result .= '<option value="' . $breadcrumb['url'] . '">' . $breadcrumb['title'] . '</option>';
}
$result .= '</select>';
$result .= '';
return $result;
}
/**
* Get the user selector.
*
* @return string The selector ready to print.
* @since 1.0.0
*/
public function get_country_selector() {
$breadcrumbs[] = [
'title' => ' - ' . esc_html__( 'All', 'vibes' ) . ' - ',
'url' => $this->get_url( [ 'country' ] ),
];
$b = [];
foreach ( $this->available_countries as $country ) {
$b[] = [
'title' => L10n::get_country_name( strtoupper( $country ) ),
'url' => $this->get_url( [], [ 'country' => $country ] ),
];
}
uasort(
$b,
function ( $a, $b ) {
if ( $a['title'] === $b['title'] ) {
return 0;
} return ( strtoupper( $a['title'] ) < strtoupper( $b['title'] ) ) ? -1 : 1;
}
);
$title = esc_html__( 'All', 'vibes' );
if ( in_array( $this->country, $this->available_countries, true ) ) {
$title = L10n::get_country_name( strtoupper( $this->country ) );
}
$result = '<select name="countries" id="countries" data="' . Feather\Icons::get_base64( 'globe', 'none', '#5A738E' ) . '" class="vibes-select countries" placeholder="' . $title . '" style="display:none;">';
foreach ( array_merge( $breadcrumbs, $b ) as $breadcrumb ) {
$result .= '<option value="' . $breadcrumb['url'] . '">' . $breadcrumb['title'] . '</option>';
}
$result .= '</select>';
$result .= '';
return $result;
}
/**
* Get the title selector.
*
* @return string The selector ready to print.
* @since 1.0.0
*/
public function get_title_selector() {
switch ( $this->type ) {
case 'domains':
$title = esc_html__( 'Domains Details', 'vibes' );
break;
case 'domain':
$title = esc_html__( 'Domain Summary', 'vibes' );
break;
case 'authorities':
$title = esc_html__( 'Domain Details', 'vibes' );
$breadcrumbs[] = [
'title' => esc_html__( 'Domain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->domain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'domain',
'domain' => $this->domain,
'id' => $this->domain,
]
),
];
break;
case 'authority':
$title = esc_html__( 'Subdomain Summary', 'vibes' );
$breadcrumbs[] = [
'title' => esc_html__( 'Domain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->domain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'domain',
'domain' => $this->domain,
'id' => $this->domain,
]
),
];
break;
case 'endpoints':
$title = esc_html__( 'Subdomain Details', 'vibes' );
$breadcrumbs[] = [
'title' => esc_html__( 'Subdomain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->subdomain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'authority',
'domain' => $this->domain,
'id' => $this->subdomain,
]
),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Domain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->domain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'domain',
'domain' => $this->domain,
'id' => $this->domain,
]
),
];
break;
case 'endpoint':
switch ( $this->extra ) {
case 'devices':
$title = esc_html__( 'Mobiles Breakdown', 'vibes' );
$breadcrumbs[] = [
'title' => esc_html__( 'Endpoint Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->id ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'endpoint',
'id' => $this->id,
]
),
];
break;
default:
$title = esc_html__( 'Endpoint Summary', 'vibes' );
}
if ( 'resource' === $this->source ) {
$breadcrumbs[] = [
'title' => esc_html__( 'Subdomain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->subdomain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'authority',
'domain' => $this->domain,
'id' => $this->subdomain,
]
),
];
$breadcrumbs[] = [
'title' => esc_html__( 'Domain Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s', 'vibes' ), $this->domain ),
'url' => $this->get_url(
[ 'extra' ],
[
'type' => 'domain',
'domain' => $this->domain,
'id' => $this->domain,
]
),
];
}
break;
case 'country':
$title = esc_html__( 'Country', 'vibes' );
$subtitle = L10n::get_country_name( $this->id );
break;
default:
switch ( $this->extra ) {
case 'devices':
$title = esc_html__( 'Mobiles Breakdown', 'vibes' );
break;
default:
$title = esc_html__( 'Main Summary', 'vibes' );
}
}
switch ( $this->source ) {
case 'webvital':
$name = esc_html__( 'Web Vitals', 'vibes' );
break;
case 'resource':
$name = esc_html__( 'Resources', 'vibes' );
break;
case 'navigation':
$name = esc_html__( 'Performances', 'vibes' );
break;
default:
$name = VIBES_PRODUCT_NAME;
}
$breadcrumbs[] = [
'title' => esc_html__( 'Main Summary', 'vibes' ),
'subtitle' => sprintf( esc_html__( 'Return to %s main page.', 'vibes' ), $name ),
'url' => $this->get_url( [ 'domain', 'id', 'extra', 'type' ] ),
];
$result = '<select name="sources" id="sources" data="" class="vibes-select sources" placeholder="' . $title . '" style="display:none;">';
foreach ( $breadcrumbs as $breadcrumb ) {
$result .= '<option value="' . $breadcrumb['url'] . '">' . $breadcrumb['title'] . '~-' . $breadcrumb['subtitle'] . '-~</option>';
}
$result .= '</select>';
$result .= '';
return $result;
}
/**
* Get the site selection bar.
*
* @return string The bar ready to print.
* @since 1.0.0
*/
public function get_site_bar() {
if ( Role::SINGLE_ADMIN === Role::admin_type() ) {
return '';
}
if ( 'all' === $this->site ) {
$result = '<span class="vibes-site-text">' . esc_html__( 'All Sites', 'vibes' ) . '</span>';
} else {
if ( Role::SUPER_ADMIN === Role::admin_type() ) {
$quit = '<a href="' . esc_url( $this->get_url( [ 'site' ] ) ) . '"><img style="width:12px;vertical-align:text-top;" src="' . Feather\Icons::get_base64( 'x-circle', 'none', '#FFFFFF' ) . '" /></a>';
$result = '<span class="vibes-site-button">' . sprintf( esc_html__( 'Site ID %s', 'vibes' ), $this->site ) . $quit . '</span>';
} else {
$result = '<span class="vibes-site-text">' . sprintf( esc_html__( 'Site ID %s', 'vibes' ), $this->site ) . '</span>';
}
}
return '<span class="vibes-site">' . $result . '</span>';
}
/**
* Get the title bar.
*
* @return string The bar ready to print.
* @since 1.0.0
*/
public function get_title_bar() {
$subtitle = $this->id;
if ( 60 < strlen( $subtitle ) ) {
$subtitle = substr( $subtitle, 0, 60 ) . '…';
}
switch ( $this->type ) {
case 'summary':
if ( '' === $this->extra ) {
$title = esc_html__( 'Main Summary', 'vibes' );
} else {
$title = $this->get_title_selector();
}
break;
case 'domain':
case 'authority':
case 'endpoint':
case 'domains':
case 'authorities':
case 'endpoints':
case 'devices':
$title = $this->get_title_selector();
break;
}
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= $this->get_site_bar();
$result .= '<span class="vibes-title">' . $title . '</span>';
$result .= '<span class="vibes-subtitle">' . $subtitle . '</span>';
if ( 'resource' !== $this->source ) {
$pickers = $this->get_country_selector() . $this->get_channel_selector() . $this->get_user_selector() . $this->get_date_box();
} else {
$pickers = $this->get_date_box();
}
$result .= '<span class="vibes-picker">' . $pickers . '</span>';
$result .= '</div>';
return $result;
}
/**
* Get the main chart.
*
* @return string The main chart ready to print.
* @since 1.0.0
*/
public function get_webvital_chart() {
if ( 1 < $this->duration ) {
$sep = '';
$detail = '';
foreach ( array_merge( WebVitals::$rated_metrics, WebVitals::$unrated_metrics ) as $metric ) {
$detail .= $sep . '<span class="vibes-chart-button not-ready left" id="vibes-chart-button-' . strtolower( $metric ) . '" data-position="left" data-tooltip="' . preg_replace( '/\(.*\)/iU', '', WebVitals::$metrics_names[ $metric ] ) . '"><span class="vibes-graph-button">' . $metric . '</span></span>';
if ( '' === $sep ) {
$sep = ' ';
}
}
$result = '<div class="vibes-row">';
$result .= '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Web Vitals Variations', 'vibes' ) . '<span class="vibes-module-more">' . $detail . '</span></span></div>';
$result .= '<div class="vibes-module-content" id="vibes-webvital-chart">' . $this->get_graph_placeholder( 274 ) . '</div>';
$result .= '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'main-webvital-chart',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
} else {
return '';
}
}
/**
* Get the main chart.
*
* @return string The main chart ready to print.
* @since 1.0.0
*/
public function get_navigation_chart() {
if ( 1 < $this->duration ) {
$help_time = esc_html__( 'Timings distribution.', 'vibes' );
$help_net = esc_html__( 'Network stages.', 'vibes' );
$detail = '<span class="vibes-chart-button not-ready left" id="vibes-chart-button-time" data-position="left" data-tooltip="' . $help_time . '"><span class="vibes-graph-button" style="padding: 1px 3px;"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'clock', 'none', '#73879C' ) . '" /></span></span>';
$detail .= ' <span class="vibes-chart-button not-ready left" id="vibes-chart-button-net" data-position="left" data-tooltip="' . $help_net . '"><span class="vibes-graph-button" style="padding: 1px 3px;"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'link', 'none', '#73879C' ) . '"/></span></span>';
$result = '<div class="vibes-row">';
$result .= '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Metrics Variations', 'vibes' ) . '<span class="vibes-module-more">' . $detail . '</span></span></div>';
$result .= '<div class="vibes-module-content" id="vibes-navigation-chart">' . $this->get_graph_placeholder( 274 ) . '</div>';
$result .= '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'main-navigation-chart',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
} else {
return '';
}
}
/**
* Get the web vital widget.
*
* @return string The widget ready to print.
* @since 1.0.0
*/
public function get_webvital_class( $class, $position ) {
switch ( $this->type ) {
case 'endpoint':
$url = $this->get_url(
[],
[
'type' => 'endpoint',
'extra' => 'devices',
]
);
break;
default:
$url = $this->get_url(
[],
[
'type' => 'summary',
'extra' => 'devices',
]
);
}
$result = '<div class="vibes-50-module vibes-50-module-' . $position . '">';
if ( 'mobile' === $class && class_exists( 'PODeviceDetector\API\Device' ) ) {
$detail = '<a href="' . esc_url( $url ) . '"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'zoom-in', 'none', '#73879C' ) . '" /></a>';
$help = esc_html__( 'View the mobiles breakdown.', 'vibes' );
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_class_name( $class ) . '</span><span class="vibes-module-more left" data-position="left" data-tooltip="' . $help . '">' . $detail . '</span></div>';
} else {
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_class_name( $class ) . '</span></div>';
}
$result .= '<div class="vibes-module-content" id="vibes-class_' . $class . '">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.class_' . $class,
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the navigation widget.
*
* @return string The widget ready to print.
* @since 1.0.0
*/
public function get_navigation_class( $class, $position ) {
switch ( $this->type ) {
case 'endpoint':
$url = $this->get_url(
[],
[
'type' => 'endpoint',
'extra' => 'devices',
]
);
break;
default:
$url = $this->get_url(
[],
[
'type' => 'summary',
'extra' => 'devices',
]
);
}
$result = '<div class="vibes-50-module-small vibes-50-module vibes-50-module-' . $position . '">';
if ( 'mobile' === $class && class_exists( 'PODeviceDetector\API\Device' ) ) {
$detail = '<a href="' . esc_url( $url ) . '"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'zoom-in', 'none', '#73879C' ) . '" /></a>';
$help = esc_html__( 'View the mobiles breakdown.', 'vibes' );
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_class_name( $class ) . '</span><span class="vibes-module-more left" data-position="left" data-tooltip="' . $help . '">' . $detail . '</span></div>';
} else {
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_class_name( $class ) . '</span></div>';
}
$result .= '<div class="vibes-module-content" id="vibes-class_' . $class . '">' . $this->get_graph_placeholder( 100 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.class_' . $class,
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the web vital widget.
*
* @return string The widget ready to print.
* @since 1.0.0
*/
public function get_webvital_device( $device, $position ) {
$result = '<div class="vibes-50-module vibes-50-module-' . $position . '">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_device_name( $device ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-device_' . $device . '">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.device_' . $device,
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the navigation widget.
*
* @return string The widget ready to print.
* @since 1.0.0
*/
public function get_navigation_device( $device, $position ) {
$result = '<div class="vibes-50-module vibes-50-module-small vibes-50-module-' . $position . '">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . Device::get_device_name( $device ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-device_' . $device . '">' . $this->get_graph_placeholder( 100 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.device_' . $device,
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the mime list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_mimes_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Mime Types Breakdown', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-mimes">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'resource.mimes' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 0,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the category list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_categories_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Sources Breakdown', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-categories">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'resource.categories' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 0,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_sites_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Sites Breakdown', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-sites">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'sites',
'queried' => 0,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_webvital_sites_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Sites Overview', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-sites">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'webvital.sites',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_navigation_sites_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Sites Overview', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-sites">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'navigation.sites',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_webvital_endpoints_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Endpoints Overview', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-endpoints">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'webvital.endpoints',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_navigation_endpoints_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Endpoints Overview', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-endpoints">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'navigation.endpoints',
'country' => $this->country,
'authent' => $this->authent,
'channel' => $this->channel,
'queried' => 0,
]
);
return $result;
}
/**
* Get the domains list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_domains_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'All Domains', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-domains">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'resource.domains',
'queried' => 0,
]
);
return $result;
}
/**
* Get the authorities list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_authorities_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'All Subdomains', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-authorities">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'authorities',
'queried' => 0,
]
);
return $result;
}
/**
* Get the endpoints list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_endpoints_list() {
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'All Endpoints', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-endpoints">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => 'endpoints',
'queried' => 0,
]
);
return $result;
}
/**
* Get the extra list.
*
* @return string The table ready to print.
* @since 1.0.0
*/
public function get_extra_list() {
switch ( $this->extra ) {
case 'devices':
$title = esc_html__( 'Mobiles Breakdown', 'vibes' );
break;
default:
$title = esc_html__( 'All Endpoints', 'vibes' );
}
$result = '<div class="vibes-box vibes-box-full-line">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . $title . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-' . $this->extra . '">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->extra,
'queried' => 0,
]
);
return $result;
}
/**
* Get the top domains box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_top_domain_box() {
$url = $this->get_url( [ 'domain' ], [ 'type' => 'domains' ] );
$detail = '<a href="' . esc_url( $url ) . '"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'zoom-in', 'none', '#73879C' ) . '" /></a>';
$help = esc_html__( 'View the details of all domains.', 'vibes' );
$result = '<div class="vibes-60-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Top Domains', 'vibes' ) . '</span><span class="vibes-module-more left" data-position="left" data-tooltip="' . $help . '">' . $detail . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-top-domains">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.top-domains',
'queried' => 5,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the top authority box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_top_authority_box() {
$url = $this->get_url(
[],
[
'type' => 'authorities',
'domain' => $this->domain,
]
);
$detail = '<a href="' . esc_url( $url ) . '"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'zoom-in', 'none', '#73879C' ) . '" /></a>';
$help = esc_html__( 'View the details of all subdomains.', 'vibes' );
$result = '<div class="vibes-60-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Top Subdomains', 'vibes' ) . '</span><span class="vibes-module-more left" data-position="left" data-tooltip="' . $help . '">' . $detail . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-top-authorities">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.top-authorities',
'queried' => 5,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the top endpoint box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_top_endpoint_box() {
$url = $this->get_url(
[],
[
'type' => 'endpoints',
'domain' => $this->domain,
]
);
$detail = '<a href="' . esc_url( $url ) . '"><img style="width:12px;vertical-align:baseline;" src="' . Feather\Icons::get_base64( 'zoom-in', 'none', '#73879C' ) . '" /></a>';
$help = esc_html__( 'View the details of all endpoints.', 'vibes' );
$result = '<div class="vibes-60-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Top Endpoints', 'vibes' ) . '</span><span class="vibes-module-more left" data-position="left" data-tooltip="' . $help . '">' . $detail . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-top-endpoints">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.top-endpoints',
'queried' => 5,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the catgory box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_category_box() {
$result = '<div class="vibes-40-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Content', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-category">' . $this->get_graph_placeholder( 200 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.category' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 7,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the initiator box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_initiator_box() {
$result = '<div class="vibes-33-module vibes-33-left-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Initiators', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-initiator">' . $this->get_graph_placeholder( 90 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.initiator' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 4,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the security box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_security_box() {
$result = '<div class="vibes-33-module vibes-33-center-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Protocols', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-security">' . $this->get_graph_placeholder( 90 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.security' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 4,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get the cache box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
public function get_cache_box() {
$result = '<div class="vibes-33-module vibes-33-right-module">';
$result .= '<div class="vibes-module-title-bar"><span class="vibes-module-title">' . esc_html__( 'Browser Cache', 'vibes' ) . '</span></div>';
$result .= '<div class="vibes-module-content" id="vibes-cache">' . $this->get_graph_placeholder( 90 ) . '</div>';
$result .= '</div>';
$result .= $this->get_refresh_script(
[
'query' => $this->source . '.cache' . ( 'none' !== $this->mode ? '~' . $this->mode : '' ),
'queried' => 4,
'domain' => $this->domain,
]
);
return $result;
}
/**
* Get a placeholder for graph.
*
* @param integer $height The height of the placeholder.
* @return string The placeholder, ready to print.
* @since 1.0.0
*/
private function get_graph_placeholder( $height ) {
return '<p style="text-align:center;line-height:' . $height . 'px;"><img style="width:40px;vertical-align:middle;" src="' . VIBES_ADMIN_URL . 'medias/bars.svg" /></p>';
}
/**
* Get refresh script.
*
* @param array $args Optional. The args for the ajax call.
* @return string The script, ready to print.
* @since 1.0.0
*/
private function get_refresh_script( $args = [] ) {
$result = '<script>';
$result .= 'jQuery(document).ready( function($) {';
$result .= ' var data = {';
$result .= ' action:"vibes_get_stats",';
$result .= ' nonce:"' . wp_create_nonce( 'ajax_vibes' ) . '",';
foreach ( $args as $key => $val ) {
$s = ' ' . $key . ':';
if ( is_string( $val ) ) {
$s .= '"' . $val . '"';
} elseif ( is_numeric( $val ) ) {
$s .= $val;
} elseif ( is_bool( $val ) ) {
$s .= $val ? 'true' : 'false';
}
$result .= $s . ',';
}
if ( '' !== $this->id ) {
$result .= ' id:"' . $this->id . '",';
}
$result .= ' type:"' . $args['query'] . '",';
$result .= ' site:"' . $this->site . '",';
$result .= ' start:"' . $this->start . '",';
$result .= ' end:"' . $this->end . '",';
$result .= ' };';
$result .= ' $.post(ajaxurl, data, function(response) {';
$result .= ' var val = JSON.parse(response);';
$result .= ' $.each(val, function(index, value) {$("#" + index).html(value);});';
if ( array_key_exists( 'query', $args ) && ( 'main-webvital-chart' === $args['query'] ) ) {
$result .= '$(".vibes-chart-button").removeClass("not-ready");';
$result .= '$("#vibes-chart-button-lcp").addClass("active");';
}
if ( array_key_exists( 'query', $args ) && ( 'main-navigation-chart' === $args['query'] ) ) {
$result .= '$(".vibes-chart-button").removeClass("not-ready");';
$result .= '$("#vibes-chart-button-time").addClass("active");';
}
$result .= ' });';
$result .= '});';
$result .= '</script>';
return $result;
}
/**
* Get the url.
*
* @param array $exclude Optional. The args to exclude.
* @param array $replace Optional. The args to replace or add.
* @return string The url.
* @since 1.0.0
*/
private function get_url( $exclude = [], $replace = [] ) {
$params = [];
$params['type'] = $this->source . '.' . $this->type . ( 'none' !== $this->mode ? '~' . $this->mode : '' );
$params['site'] = $this->site;
$params['domain'] = $this->domain;
if ( 'resource' !== $this->source ) {
$params['authent'] = $this->authent;
$params['country'] = $this->country;
$params['channel'] = $this->channel;
}
if ( '' !== $this->id ) {
$params['id'] = $this->id;
}
if ( '' !== $this->extra ) {
$params['extra'] = $this->extra;
}
$params['start'] = $this->start;
$params['end'] = $this->end;
foreach ( $exclude as $arg ) {
unset( $params[ $arg ] );
}
foreach ( $replace as $key => $arg ) {
if ( 'type' === $key ) {
$params[ $key ] = $this->source . '.' . $arg;
} else {
$params[ $key ] = $arg;
}
}
$url = admin_url( 'admin.php?page=vibes-' . $this->source . '-viewer' );
foreach ( $params as $key => $arg ) {
if ( '' !== $arg ) {
$url .= '&' . $key . '=' . $arg;
}
}
return $url;
}
/**
* Get a date picker box.
*
* @return string The box ready to print.
* @since 1.0.0
*/
private function get_date_box() {
$result = '<span class="vibes-datepicker"><img style="width:13px;vertical-align:middle;" src="' . Feather\Icons::get_base64( 'calendar', 'none', '#5A738E' ) . '" /> <span class="vibes-datepicker-value"></span></span>';
$result .= '<script>';
$result .= 'jQuery(function ($) {';
$result .= ' moment.locale("' . L10n::get_display_locale() . '");';
$result .= ' var start = moment("' . $this->start . '");';
$result .= ' var end = moment("' . $this->end . '");';
$result .= ' function changeDate(start, end) {';
$result .= ' $("span.vibes-datepicker-value").html(start.format("LL") + " - " + end.format("LL"));';
$result .= ' }';
$result .= ' $(".vibes-datepicker").daterangepicker({';
$result .= ' opens: "left",';
$result .= ' startDate: start,';
$result .= ' endDate: end,';
$result .= ' minDate: moment("' . Schema::get_oldest_date( $this->source ) . '"),';
$result .= ' maxDate: moment(),';
$result .= ' showCustomRangeLabel: true,';
$result .= ' alwaysShowCalendars: true,';
$result .= ' locale: {customRangeLabel: "' . esc_html__( 'Custom Range', 'vibes' ) . '",cancelLabel: "' . esc_html__( 'Cancel', 'vibes' ) . '", applyLabel: "' . esc_html__( 'Apply', 'vibes' ) . '"},';
$result .= ' ranges: {';
$result .= ' "' . esc_html__( 'Today', 'vibes' ) . '": [moment(), moment()],';
$result .= ' "' . esc_html__( 'Yesterday', 'vibes' ) . '": [moment().subtract(1, "days"), moment().subtract(1, "days")],';
$result .= ' "' . esc_html__( 'This Month', 'vibes' ) . '": [moment().startOf("month"), moment().endOf("month")],';
$result .= ' "' . esc_html__( 'Last Month', 'vibes' ) . '": [moment().subtract(1, "month").startOf("month"), moment().subtract(1, "month").endOf("month")],';
$result .= ' }';
$result .= ' }, changeDate);';
$result .= ' changeDate(start, end);';
$result .= ' $(".vibes-datepicker").on("apply.daterangepicker", function(ev, picker) {';
$result .= ' var url = "' . $this->get_url( [ 'start', 'end' ], [ 'domain' => $this->domain ] ) . '" + "&start=" + picker.startDate.format("YYYY-MM-DD") + "&end=" + picker.endDate.format("YYYY-MM-DD");';
$result .= ' $(location).attr("href", url.replaceAll("&", String.fromCharCode(38)));';
$result .= ' });';
$result .= '});';
$result .= '</script>';
return $result;
}
}