Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Event Tracking Features #30

Merged
merged 23 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
da146fd
Add Schema class for managing database tables
jrtashjian May 29, 2024
5077c4f
Add AnalyticsServiceProvider for managing analytics feature
jrtashjian May 29, 2024
1fc8ada
Load AnalyticsServiceProvider
jrtashjian May 29, 2024
c6dff4d
Add QueryBuilder class for managing database queries
jrtashjian May 29, 2024
5af8cd3
Add DB facade for managing database queries
jrtashjian May 30, 2024
115277e
Trim stats to a single simple table
jrtashjian May 30, 2024
dd8429e
Add AnalyticsManager class and daily salt generation
jrtashjian May 30, 2024
22133d6
Add group by clause to QueryBuilder class
jrtashjian Jun 15, 2024
1016cf3
Add count method to QueryBuilder class
jrtashjian Jun 15, 2024
556c5eb
Add QueryBuilderFactory class for managing database queries
jrtashjian Jun 15, 2024
ae15e94
Continue building out AnalyticsManager
jrtashjian Jun 15, 2024
13e0fc6
Update AnalyticsServiceProvider to use QueryBuilderFactory for databa…
jrtashjian Jun 15, 2024
70c5800
Record form events with new AnalyticsManager class
jrtashjian Jun 15, 2024
b5b8460
Add Number class for formatting numbers
jrtashjian Jun 15, 2024
c9767e6
Refactor CPT columns to use AnalyticsManager.
jrtashjian Jun 15, 2024
ebcf6d3
Add i18n and escaping
jrtashjian Jun 15, 2024
35249cb
phpcs fix
jrtashjian Jun 15, 2024
b3b0e1f
Optimize with separate visitor_hash table
jrtashjian Jun 15, 2024
9bd5c41
Update record impression logic
jrtashjian Jun 15, 2024
3219a7a
Prevent submission on unpublished forms
jrtashjian Jun 15, 2024
9e9b2e9
Display new errors on submission
jrtashjian Jun 15, 2024
5c2e774
Fix conditions for recording events
jrtashjian Jun 15, 2024
0505394
Update deps
jrtashjian Jun 15, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
524 changes: 289 additions & 235 deletions composer.lock

Large diffs are not rendered by default.

206 changes: 206 additions & 0 deletions includes/Analytics/AnalyticsManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php
/**
* The AnalyticsManager class.
*
* @package OmniForm
*/

namespace OmniForm\Analytics;

use OmniForm\Plugin\QueryBuilderFactory;

/**
* The AnalyticsManager class.
*/
class AnalyticsManager {
/**
* The QueryBuilder instance.
*
* @var QueryBuilder
*/
protected $query_builder_factory;

/**
* The daily salt.
*
* @var string
*/
protected $daily_salt;

/**
* The AnalyticsManager constructor.
*
* @param QueryBuilderFactory $query_builder_factory The QueryBuilderFactory instance.
* @param string $daily_salt The daily salt.
*/
public function __construct( QueryBuilderFactory $query_builder_factory, string $daily_salt ) {
$this->query_builder_factory = $query_builder_factory;
$this->daily_salt = $daily_salt;
}

/**
* Get the user agent.
*
* @return string The user agent.
*/
protected function get_user_agent() {
return isset( $_SERVER['HTTP_USER_AGENT'] ) ? $_SERVER['HTTP_USER_AGENT'] : '';
}

/**
* Get the IP address.
*
* @return string The IP address.
*/
protected function get_ip_address() {
return isset( $_SERVER['REMOTE_ADDR'] ) ? $_SERVER['REMOTE_ADDR'] : '';
}

/**
* Get the visitor hash.
*
* @return string The visitor hash.
*/
protected function get_visitor_hash() {
return hash( 'sha256', $this->daily_salt . $this->get_ip_address() . $this->get_user_agent() );
}

/**
* Record an event.
*
* @param int $form_id The form ID.
* @param int $event_type The event type.
*/
protected function record_event( int $form_id, int $event_type ) {
$query_builder = $this->query_builder_factory->create();

$query_builder->table( AnalyticsServiceProvider::EVENTS_TABLE )
->insert(
array(
'form_id' => $form_id,
'event_type' => $event_type,
'visitor_id' => $this->get_visitor_id(),
'event_time' => current_time( 'mysql' ),
)
);
}

/**
* Get the visitor ID from the visitor hash.
*
* @return int The visitor ID.
*/
protected function get_visitor_id() {
$query_builder = $this->query_builder_factory->create();

$visitor_results = $query_builder->table( AnalyticsServiceProvider::VISITOR_TABLE )
->select( 'visitor_id' )
->where( 'visitor_hash', '=', $this->get_visitor_hash() )
->get();

if ( empty( $visitor_results ) ) {
$query_builder->table( AnalyticsServiceProvider::VISITOR_TABLE )
->insert(
array(
'visitor_hash' => $this->get_visitor_hash(),
)
);

return $query_builder->get_last_insert_id();
}

return $visitor_results[0]->visitor_id;
}

/**
* Record an impression.
*
* @param int $form_id The form ID.
*/
public function record_impression( int $form_id ) {
$this->record_event( $form_id, EventType::IMPRESSION );
}

/**
* Record a successful submission.
*
* @param int $form_id The form ID.
*/
public function record_submission_success( int $form_id ) {
$this->record_event( $form_id, EventType::SUBMISSION_SUCCESS );
}

/**
* Record a failed submission.
*
* @param int $form_id The form ID.
*/
public function record_submission_failure( int $form_id ) {
$this->record_event( $form_id, EventType::SUBMISSION_FAILURE );
}

/**
* Get the impression count.
*
* @param int $form_id The form ID.
* @param bool $unique Whether to count unique impressions.
*
* @return int The impression count.
*/
public function get_impression_count( int $form_id, bool $unique = false ) {
$query_builder = $this->query_builder_factory->create();

return $query_builder->table( AnalyticsServiceProvider::EVENTS_TABLE )
->where( 'form_id', '=', $form_id )
->where( 'event_type', '=', EventType::IMPRESSION )
->count( $unique ? 'DISTINCT visitor_id' : 'event_id' );
}

/**
* Get the submission count.
*
* @param int $form_id The form ID.
* @param bool $unique Whether to count unique submissions.
*
* @return int The submission count.
*/
public function get_submission_count( int $form_id, bool $unique = false ) {
$query_builder = $this->query_builder_factory->create();

return $query_builder->table( AnalyticsServiceProvider::EVENTS_TABLE )
->where( 'form_id', '=', $form_id )
->where( 'event_type', '=', EventType::SUBMISSION_SUCCESS )
->count( $unique ? 'DISTINCT visitor_id' : 'event_id' );
}

/**
* Get the failed submission count.
*
* @param int $form_id The form ID.
* @param bool $unique Whether to count unique failed submissions.
*
* @return int The failed submission count.
*/
public function get_failed_submission_count( int $form_id, bool $unique = false ) {
$query_builder = $this->query_builder_factory->create();

return $query_builder->table( AnalyticsServiceProvider::EVENTS_TABLE )
->where( 'form_id', '=', $form_id )
->where( 'event_type', '=', EventType::SUBMISSION_FAILURE )
->count( $unique ? 'DISTINCT visitor_id' : 'event_id' );
}

/**
* Get the conversion rate.
*
* @param int $form_id The form ID.
*
* @return float The conversion rate.
*/
public function get_conversion_rate( int $form_id ) {
$impressions = $this->get_impression_count( $form_id, true );
$submissions = $this->get_submission_count( $form_id, true );

return $impressions > 0 ? ( $submissions / $impressions ) : 0;
}
}
111 changes: 111 additions & 0 deletions includes/Analytics/AnalyticsServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php
/**
* The AnalyticsServiceProvider class.
*
* @package OmniForm
*/

namespace OmniForm\Analytics;

use OmniForm\Dependencies\League\Container\ServiceProvider\AbstractServiceProvider;
use OmniForm\Dependencies\League\Container\ServiceProvider\BootableServiceProviderInterface;
use OmniForm\Plugin\QueryBuilderFactory;
use OmniForm\Plugin\Schema;

/**
* The AnalyticsServiceProvider class.
*/
class AnalyticsServiceProvider extends AbstractServiceProvider implements BootableServiceProviderInterface {
const EVENTS_TABLE = 'omniform_stats_events';
const VISITOR_TABLE = 'omniform_stats_visitors';

/**
* Get the services provided by the provider.
*
* @param string $id The service to check.
*
* @return array
*/
public function provides( string $id ): bool {
$services = array(
AnalyticsManager::class,
);

return in_array( $id, $services, true );
}

/**
* Register any application services.
*
* @return void
*/
public function register(): void {
$this->getContainer()->addShared(
AnalyticsManager::class,
function () {
return new AnalyticsManager(
$this->getContainer()->get( QueryBuilderFactory::class ),
$this->generate_daily_salt()
);
}
);
}

/**
* Bootstrap any application services by hooking into WordPress with actions/filters.
*
* @return void
*/
public function boot(): void {
add_action( 'omniform_activate', array( $this, 'activate' ) );
}

/**
* Generate the daily salt.
*
* @return string
*/
public function generate_daily_salt() {
$daily_salt = get_transient( 'omniform_analytics_salt' );

if ( false === $daily_salt ) {
$salt = wp_generate_password( 64, true, true );
$daily_salt = hash( 'sha256', $salt . gmdate( 'Y-m-d' ) );

set_transient( 'omniform_analytics_salt', $daily_salt, DAY_IN_SECONDS );
}

return $daily_salt;
}

/**
* Initialize the analytics tables.
*/
public function activate() {
$events_table_definition = array(
'`event_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT',
'`form_id` BIGINT(20) UNSIGNED NOT NULL',
'`visitor_id` BIGINT(20) UNSIGNED NOT NULL',
'`event_type` TINYINT(1) UNSIGNED NOT NULL',
'`event_time` DATETIME NOT NULL',
'PRIMARY KEY (`event_id`)',
'INDEX (`form_id`)',
'INDEX (`visitor_id`)',
'INDEX (`event_type`)',
);

if ( ! Schema::has_table( self::EVENTS_TABLE ) ) {
Schema::create( self::EVENTS_TABLE, $events_table_definition );
}

$visitors_table_definition = array(
'`visitor_id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT',
'`visitor_hash` CHAR(64) NOT NULL',
'PRIMARY KEY (`visitor_id`)',
);

if ( ! Schema::has_table( self::VISITOR_TABLE ) ) {
Schema::create( self::VISITOR_TABLE, $visitors_table_definition );
}
}
}
18 changes: 18 additions & 0 deletions includes/Analytics/EventType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/**
* The EventType class.
*
* @package OmniForm
*/

namespace OmniForm\Analytics;

/**
* The EventType class.
*/
class EventType {
const IMPRESSION = 0;

const SUBMISSION_SUCCESS = 1;
const SUBMISSION_FAILURE = 2;
}
12 changes: 12 additions & 0 deletions includes/Plugin/Api/FormsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public function create_response( \WP_REST_Request $request ) {
);
}

if ( ! $form->is_published() ) {
return new \WP_Error(
'omniform_not_published',
esc_html__( 'The form is not published.', 'omniform' ),
array( 'status' => 400 )
);
}

// Prepare the submitted data.
$prepared_response_data = $this->sanitize_array(
$request->get_params()
Expand All @@ -71,6 +79,10 @@ public function create_response( \WP_REST_Request $request ) {
'invalid_fields' => $errors,
);

if ( ! current_user_can( 'edit_theme_options' ) ) {
omniform()->get( \OmniForm\Analytics\AnalyticsManager::class )->record_submission_failure( $form->get_id() );
}

return rest_ensure_response(
new \WP_HTTP_Response( $response, $response['status'] )
);
Expand Down
Loading
Loading