/admin/dashboards/class-admin-dashboards-collector.php

  1. <?php 
  2. /** 
  3. * @package GoogleAnalytics\Admin 
  4. */ 
  5.  
  6. /** 
  7. * Dashboards collector. 
  8. */ 
  9. class Yoast_GA_Dashboards_Collector { 
  10.  
  11. /** 
  12. * @var array $active_metrics Store the active metrics 
  13. */ 
  14. public $active_metrics; 
  15.  
  16. /** 
  17. * Store the dimensions 
  18. * 
  19. * @var array 
  20. */ 
  21. private $dimensions = array(); 
  22.  
  23. /** 
  24. * Store the valid metrics, which should be 
  25. * 
  26. * @var array 
  27. */ 
  28. private $valid_metrics = array(); 
  29.  
  30. /** 
  31. * @var integer $ga_profile_id Store the GA Profile ID 
  32. */ 
  33. public $ga_profile_id; 
  34.  
  35. /** 
  36. * Construct on the dashboards class for GA 
  37. * 
  38. * @param int $ga_profile_id 
  39. * @param array $active_metrics 
  40. * @param array $valid_metrics 
  41. */ 
  42. public function __construct( $ga_profile_id, $active_metrics, $valid_metrics ) { 
  43. $this->ga_profile_id = $ga_profile_id; 
  44.  
  45. $active_metrics = $this->filter_metrics_to_dimensions( $active_metrics ); 
  46. $this->active_metrics = $active_metrics; 
  47.  
  48. add_filter( 'ga_dashboards_dimensions', array( $this, 'filter_dimensions' ), 10, 1 ); 
  49.  
  50. $this->options = Yoast_GA_Dashboards_Api_Options::get_instance(); 
  51.  
  52. $this->init_shutdown_hook(); 
  53.  
  54. /** 
  55. * Fetch the data from Google Analytics and store it 
  56. */ 
  57. public function aggregate_data() { 
  58. if ( is_numeric( $this->ga_profile_id ) ) { 
  59. // ProfileID is set 
  60.  
  61. /** 
  62. * Implement the metric data first 
  63. */ 
  64. if ( is_array( $this->active_metrics ) && count( $this->active_metrics ) >= 1 ) { 
  65. $this->aggregate_metrics( $this->active_metrics ); 
  66.  
  67. /** 
  68. * Now implement the dimensions that are set 
  69. */ 
  70. if ( is_array( $this->dimensions ) && count( $this->dimensions ) >= 1 ) { 
  71. $this->aggregate_dimensions( $this->dimensions ); 
  72. else { 
  73. // Failure on authenticating, please reauthenticate 
  74.  
  75. /** 
  76. * This hook runs on the shutdown to fetch data from GA 
  77. */ 
  78. private function init_shutdown_hook() { 
  79. // Hook the WP cron event 
  80. add_action( 'wp', array( $this, 'setup_wp_cron_aggregate' ) ); 
  81.  
  82. // Hook our function to the WP cron event the fetch data daily 
  83. add_action( 'yst_ga_aggregate_data', array( $this, 'aggregate_data' ) ); 
  84.  
  85. // Check if the WP cron did run on time 
  86. if ( filter_input( INPUT_GET, 'page' ) === 'yst_ga_dashboard' ) { 
  87. add_action( 'shutdown', array( $this, 'check_api_call_hook' ) ); 
  88.  
  89. /** 
  90. * Check if we scheduled the WP cron event, if not, do so. 
  91. */ 
  92. public function setup_wp_cron_aggregate() { 
  93. if ( ! wp_next_scheduled( 'yst_ga_aggregate_data' ) ) { 
  94. // Set the next event of fetching data 
  95. wp_schedule_event( strtotime( date( 'Y-m-d', strtotime( 'tomorrow' ) ) . ' 00:05:00 ' ), 'daily', 'yst_ga_aggregate_data' ); 
  96.  
  97. /** 
  98. * Check if the WP cron did run yesterday. If not, we need to run it form here 
  99. */ 
  100. public function check_api_call_hook() { 
  101. $last_run = $this->get_last_aggregate_run(); 
  102.  
  103.  
  104. /** 
  105. * Transient doesn't exists, so we need to run the 
  106. * hook (This function runs already on Shutdown so 
  107. * we can call it directly from now on) or the last run has ben more than 24 hours 
  108. */ 
  109. if ( $last_run === false || Yoast_GA_Utils::hours_between( strtotime( $last_run ), time() ) >= 24 ) { 
  110. $this->aggregate_data(); 
  111.  
  112.  
  113. /** 
  114. * Get the datetime when the aggregate data function was succesful 
  115. * 
  116. * @return mixed 
  117. */ 
  118. private function get_last_aggregate_run() { 
  119. return get_option( 'yst_ga_last_wp_run' ); 
  120.  
  121. /** 
  122. * Remove metrics and set them as a dimension if needed 
  123. * 
  124. * @param array $metrics 
  125. * 
  126. * @return mixed 
  127. */ 
  128. private function filter_metrics_to_dimensions( $metrics ) { 
  129. $filter_metrics = $this->get_filter_metrics(); 
  130.  
  131. foreach ( $metrics as $key => $metric_name ) { 
  132. if ( isset( $filter_metrics[ $metric_name ] ) ) { 
  133. // Add and set the dimension 
  134. $dimension = array( $filter_metrics[ $metric_name ] ); 
  135. $this->dimensions = array_merge( $this->dimensions, $dimension ); 
  136.  
  137. // Remove it from the metrics after we've added it into dimensions 
  138. unset( $metrics[ $key ] ); 
  139.  
  140. return $metrics; 
  141.  
  142. /** 
  143. * Get array with metrics which we need to filter as a dimension 
  144. * 
  145. * @return array 
  146. */ 
  147. private function get_filter_metrics() { 
  148. return array( 
  149. 'source' => array( 
  150. 'metric' => 'sessions',  
  151. 'dimension' => 'source',  
  152. 'storage_name' => 'source',  
  153. ),  
  154. 'top_pageviews' => array( 
  155. 'metric' => 'pageViews',  
  156. 'dimension' => 'pagePath',  
  157. 'storage_name' => 'top_pageviews',  
  158. ),  
  159. 'top_countries' => array( 
  160. 'metric' => 'sessions',  
  161. 'dimension' => 'country',  
  162. 'storage_name' => 'top_countries',  
  163. ),  
  164. ); 
  165.  
  166. /** 
  167. * Filter function for adding dimensions 
  168. * 
  169. * @filter ga_dashboards_dimensions 
  170. * 
  171. * @param array $dimensions 
  172. * 
  173. * @return array 
  174. */ 
  175. public function filter_dimensions( $dimensions = array() ) { 
  176. if ( is_array( $dimensions ) && count( $dimensions ) >= 1 ) { 
  177. $dimensions = array_merge( $this->dimensions, $dimensions ); 
  178. $this->dimensions = $dimensions; 
  179.  
  180. return $this->dimensions; 
  181.  
  182. /** 
  183. * Get the start and and date for aggregation functionality 
  184. * 
  185. * @return array 
  186. */ 
  187. private function get_date_range() { 
  188. /** 
  189. * Filter: 'yst-ga-filter-api-end-date' - Allow people to change the end date for the dashboard 
  190. * data. Default: yesterday. 
  191. * 
  192. * @api string Date (Y-m-d) 
  193. */ 
  194. return array( 
  195. 'start' => date( 'Y-m-d', strtotime( '-1 month' ) ),  
  196. 'end' => apply_filters( 'yst-ga-filter-api-end-date', date( 'Y-m-d', strtotime( 'yesterday' ) ) ),  
  197. ); 
  198.  
  199. /** 
  200. * Aggregate metrics from GA. This function should be called in the shutdown function. 
  201. * 
  202. * @param array $metrics 
  203. */ 
  204. private function aggregate_metrics( $metrics ) { 
  205. $dates = $this->get_date_range(); 
  206.  
  207. foreach ( $metrics as $metric ) { 
  208. $this->execute_call( $metric, $dates['start'], $dates['end'] ); 
  209.  
  210. /** 
  211. * Aggregate dimensions from GA. This function should be called in the shutdown function. 
  212. * 
  213. * @param array $dimensions 
  214. */ 
  215. private function aggregate_dimensions( $dimensions ) { 
  216. $dates = $this->get_date_range(); 
  217.  
  218. foreach ( $dimensions as $dimension ) { 
  219. if ( isset( $dimension['metric'] ) ) { 
  220. if ( isset( $dimension['id'] ) ) { 
  221. $this->execute_call( $dimension['metric'], $dates['start'], $dates['end'], 'ga:dimension' . $dimension['id'] ); 
  222. elseif ( isset( $dimension['dimension'] ) ) { 
  223. if ( isset( $dimension['storage_name'] ) ) { 
  224. $this->execute_call( $dimension['metric'], $dates['start'], $dates['end'], 'ga:' . $dimension['dimension'], $dimension['storage_name'] ); 
  225. else { 
  226. $this->execute_call( $dimension['metric'], $dates['start'], $dates['end'], 'ga:' . $dimension['dimension'] ); 
  227.  
  228. /** 
  229. * Execute an API call to Google Analytics and store the data in the dashboards data class 
  230. * 
  231. * @param string $metric 
  232. * @param string $start_date 2014-10-16 
  233. * @param string $end_date 2014-11-20 
  234. * @param string $dimensions ga:date 
  235. * @param string $storage_name auto 
  236. * 
  237. * @return bool 
  238. */ 
  239. private function execute_call( $metric, $start_date, $end_date, $dimensions = 'ga:date', $storage_name = 'auto' ) { 
  240. $dimensions = $this->prepare_dimensions( $dimensions, $metric ); 
  241. $params = $this->build_params_for_call( $start_date, $end_date, $dimensions, $metric ); 
  242. $storage_type = $this->get_storage_type( $dimensions ); 
  243.  
  244. $response = Yoast_Google_Analytics::get_instance()->do_request( 'https://www.googleapis.com/analytics/v3/data/ga?' . $params ); 
  245.  
  246. if ( isset( $response['response']['code'] ) && $response['response']['code'] == 200 ) { 
  247.  
  248. // Delete option api_fail because there it's successful now 
  249. delete_option( 'yst_ga_api_call_fail' ); 
  250.  
  251. // Success, set a transient which stores the latest runtime 
  252. update_option( 'yst_ga_last_wp_run', date( 'Y-m-d' ) ); 
  253.  
  254. $response = Yoast_Googleanalytics_Reporting::get_instance()->parse_response( $response, $storage_type, $start_date, $end_date ); 
  255. else { 
  256. // When response is failing, we should count the number of 
  257. $this->save_api_failure(); 
  258.  
  259. return false; 
  260.  
  261. if ( strpos( 'ga:date', $dimensions ) !== false ) { 
  262. return $this->handle_response( $response, $metric, $dimensions, $start_date, $end_date, 'datelist', $storage_name ); 
  263. else { 
  264. return $this->handle_response( $response, $metric, $dimensions, $start_date, $end_date, 'table', $storage_name ); 
  265.  
  266. /** 
  267. * When the API isn't able to get a successful response (code 200), we have to save that the call has failed 
  268. */ 
  269. private function save_api_failure() { 
  270. update_option( 'yst_ga_api_call_fail', true ); 
  271.  
  272. /** 
  273. * Get the storage type from dimensions 
  274. * 
  275. * @param string $dimensions 
  276. * 
  277. * @return string 
  278. */ 
  279. private function get_storage_type( $dimensions ) { 
  280. if ( strpos( 'ga:date', $dimensions ) !== false ) { 
  281. return 'datelist'; 
  282. else { 
  283. return 'table'; 
  284.  
  285. /** 
  286. * Prepare dimensions before adding them as a parameter in a call 
  287. * 
  288. * @param array $dimensions 
  289. * @param array $metric 
  290. * 
  291. * @return array 
  292. */ 
  293. private function prepare_dimensions( $dimensions, $metric ) { 
  294. $filter_metrics = $this->get_filter_metrics(); 
  295.  
  296. // Check if the dimensions param is an array, if so, glue it with implode to a comma separated string. 
  297. if ( is_array( $dimensions ) ) { 
  298. $dimensions = implode( ', ', $dimensions ); 
  299.  
  300. if ( in_array( $metric, $this->valid_metrics ) ) { 
  301. $dimensions = 'ga:date, ' . $dimensions; 
  302. elseif ( isset( $filter_metrics[ str_replace( 'ga:', '', $dimensions ) ] ) ) { 
  303. // Make sure we don't have a ga:date property here 
  304. $dimensions = str_replace( 'ga:date', '', $dimensions ); 
  305.  
  306. return $dimensions; 
  307.  
  308. /** 
  309. * Build the params for a call to Google Analytics, return them prepared for a http query 
  310. * 
  311. * @param string $start_date 
  312. * @param string $end_date 
  313. * @param string $dimensions 
  314. * @param string $metric 
  315. * 
  316. * @return string 
  317. */ 
  318. private function build_params_for_call( $start_date, $end_date, $dimensions, $metric ) { 
  319. /** 
  320. * Filter: 'yst-ga-filter-api-limit' - Allow people to change the max results value in the API 
  321. * calls. Default value is 1000 results per call. 
  322. * 
  323. * @api int 1000 
  324. */ 
  325. $api_call_limit = apply_filters( 'yst-ga-filter-api-limit', 1000 ); 
  326.  
  327. $params = array( 
  328. 'ids' => 'ga:' . $this->ga_profile_id,  
  329. 'start-date' => $start_date,  
  330. 'end-date' => $end_date,  
  331. 'dimensions' => $dimensions,  
  332. 'metrics' => 'ga:' . $metric,  
  333. 'max-results' => $api_call_limit,  
  334. ); 
  335.  
  336. $params = $this->add_sort_direction( $params, $dimensions, $metric ); 
  337. $params = http_build_query( $params ); 
  338.  
  339. return $params; 
  340.  
  341. /** 
  342. * Add a sort direction if we need to (Especially on dimensions which are 
  343. * listed in $this->get_filter_metrics()) 
  344. * 
  345. * @param array $params 
  346. * @param string $dimensions 
  347. * @param string $metric 
  348. * 
  349. * @return array 
  350. */ 
  351. private function add_sort_direction( $params, $dimensions, $metric ) { 
  352. $filter_dimensions = $this->get_filter_metrics(); 
  353.  
  354. foreach ( $filter_dimensions as $dimension ) { 
  355. if ( str_replace( 'ga:', '', $dimensions ) == $dimension['dimension'] && str_replace( 'ga:', '', $metric ) == $dimension['metric'] ) { 
  356. $params['sort'] = '-ga:' . $dimension['metric']; 
  357.  
  358. return $params; 
  359.  
  360. /** 
  361. * Handle the response from the Google Analytics api. 
  362. * 
  363. * @param array|boolean $response 
  364. * @param string $metric 
  365. * @param array $dimensions 
  366. * @param string $start_date 
  367. * @param string $end_date 
  368. * @param string $store_as 
  369. * @param string $storage_name 
  370. * 
  371. * @return bool 
  372. */ 
  373. private function handle_response( $response, $metric, $dimensions, $start_date, $end_date, $store_as = 'table', $storage_name = 'auto' ) { 
  374. if ( is_array( $response ) ) { 
  375. // Success, store this data 
  376. $filter_metrics = $this->get_filter_metrics(); 
  377. $extracted = str_replace( 'ga:', '', str_replace( 'ga:date, ', '', $dimensions ) ); 
  378.  
  379. if ( isset( $filter_metrics[ $extracted ] ) ) { 
  380. $name = $extracted; 
  381. else { 
  382. $name = $metric; 
  383.  
  384. if ( $dimensions !== 'ga:date' && ! isset( $filter_metrics[ $extracted ] ) ) { 
  385. $name = str_replace( 'ga:date, ', '', $dimensions ); 
  386.  
  387. // Overwrite the name if we have a defined one 
  388. if ( $storage_name != 'auto' ) { 
  389. $name = $storage_name; 
  390.  
  391. return Yoast_GA_Dashboards_Data::set( $name, $response, strtotime( $start_date ), strtotime( $end_date ), $store_as ); 
  392. else { 
  393. // Failure on API call try to log it 
  394. $this->log_error( print_r( $response, true ) ); 
  395.  
  396. return false; 
  397.  
  398. /** 
  399. * Log an error while calling the Google Analytics API 
  400. * 
  401. * @param string $error 
  402. */ 
  403. private function log_error( $error ) { 
  404. if ( true == WP_DEBUG ) { 
  405. if ( function_exists( 'error_log' ) ) { 
  406. error_log( 'Google Analytics by Yoast (Dashboard API): ' . $error ); 
  407.  
.