Skip to content

Instantly share code, notes, and snippets.

@bsmirnov
Created February 20, 2025 15:05
Show Gist options
  • Save bsmirnov/7efc41c21d6e34e69b3d864131b7be07 to your computer and use it in GitHub Desktop.
Save bsmirnov/7efc41c21d6e34e69b3d864131b7be07 to your computer and use it in GitHub Desktop.
Form Tracking

Problem Statement A Drupal website includes a webform tool that allows users (likely healthcare professionals) to perform financial calculations for kidney transplant programs. The website owners need to track user engagement with minimal personal data collection. Key Requirements:

Track when a user selects a provider/institution from the dropdown (first form field) Record which calculation tools the user shows interest in:

Living donation cost benefits Kidney paired exchange cost benefits Staff cost analysis

Track if the user completes and submits the form Track if the user downloads the generated report Create distinct records for different users from the same institution Prevent duplicate records for the same user visiting multiple times Provide administrators with a dashboard to view this tracking data

Form Structure:

Initial dropdown to select a provider/institution (required field) Three checkbox options that reveal different calculation tools Each tool contains additional form fields (not to be tracked individually) Form generates a downloadable report

Data Privacy Constraints:

Do not track specific financial data entered Do not track personal identifying information Only track institution selection and user interaction patterns

Desired Outcome A tracking system that:

Creates a record when a provider is first selected via AJAX Updates the record to show which calculation tools were explored Flags when a form is submitted Flags when a report is downloaded Uses session tracking to differentiate between users while maintaining privacy Provides administrators with actionable data on institutional interest and tool usage Functions through Drupal's webform module with custom extensions

This will allow administrators to follow up with institutions showing interest in specific kidney transplant program analysis tools.

Revised Solution

1. Create a Custom Entity for Basic Tracking

/**
 * Fields for the InstitutionTracking entity:
 * - provider (string, required): The selected institution/hospital
 * - session_id (string): To identify unique sessions
 * - ip_address (string): Additional identifier for unique visits
 * - timestamp (datetime): When the tracking began
 * - interests (array): Which option checkboxes they selected
 *   - living_donation (boolean)
 *   - paired_exchange (boolean)
 *   - staff_cost (boolean)
 * - form_submitted (boolean): If they completed and submitted the form
 * - report_downloaded (boolean): If they downloaded the report
 */

2. JavaScript/AJAX Trigger for Initial Selection

(function ($, Drupal) {
  Drupal.behaviors.webformInstitutionTracking = {
    attach: function (context, settings) {
      // Target the provider select field
      $('.provider-select', context).once('institution-tracking').change(function() {
        var selectedProvider = $(this).val();
        
        // Only trigger if an actual provider is selected (not the "- Select -" option)
        if (selectedProvider && selectedProvider !== '- Select -') {
          // Call AJAX endpoint to record the initial selection
          $.ajax({
            url: '/webform-tracking/record',
            type: 'POST',
            data: {
              'provider': selectedProvider,
              'webform_id': settings.webformId
            },
            success: function(response) {
              // Store the tracking ID in browser session storage
              if (response.tracking_id) {
                sessionStorage.setItem('webform_tracking_id', response.tracking_id);
              }
            }
          });
        }
      });
      
      // Track checkbox selections for interest areas
      $('.interest-checkbox', context).once('interest-tracking').change(function() {
        var trackingId = sessionStorage.getItem('webform_tracking_id');
        var interestField = $(this).attr('name');
        var isChecked = $(this).is(':checked');
        
        if (trackingId) {
          $.ajax({
            url: '/webform-tracking/update-interest',
            type: 'POST',
            data: {
              'tracking_id': trackingId,
              'interest_field': interestField,
              'value': isChecked
            }
          });
        }
      });
    }
  };
})(jQuery, Drupal);

3. AJAX Controller for Recording Initial Selection

/**
 * Records the initial provider selection.
 */
public function recordInitialSelection(Request $request) {
  $provider = $request->request->get('provider');
  $webform_id = $request->request->get('webform_id');
  $session_id = $request->getSession()->getId();
  $ip_address = $request->getClientIp();
  
  // Check if we already have a record for this session and IP within the last day
  $existing = \Drupal::entityTypeManager()
    ->getStorage('institution_tracking')
    ->loadByProperties([
      'session_id' => $session_id,
      'ip_address' => $ip_address,
      'timestamp' => \Drupal::time()->getRequestTime() - 86400, // Last 24 hours
    ]);
  
  // If no existing record found, create a new one
  if (empty($existing)) {
    $tracking = \Drupal\webform_institution_tracking\Entity\InstitutionTracking::create([
      'provider' => $provider,
      'session_id' => $session_id,
      'ip_address' => $ip_address,
      'timestamp' => \Drupal::time()->getRequestTime(),
      'interests' => [
        'living_donation' => FALSE,
        'paired_exchange' => FALSE,
        'staff_cost' => FALSE,
      ],
      'form_submitted' => FALSE,
      'report_downloaded' => FALSE,
    ]);
    $tracking->save();
    
    return new JsonResponse(['tracking_id' => $tracking->id()]);
  } else {
    $tracking = reset($existing);
    return new JsonResponse(['tracking_id' => $tracking->id()]);
  }
}

4. Update Interests Controller

/**
 * Updates which interest areas the user selected.
 */
public function updateInterests(Request $request) {
  $tracking_id = $request->request->get('tracking_id');
  $interest_field = $request->request->get('interest_field');
  $value = (bool) $request->request->get('value');
  
  if ($tracking_id && $interest_field) {
    $tracking = \Drupal\webform_institution_tracking\Entity\InstitutionTracking::load($tracking_id);
    if ($tracking) {
      $interests = $tracking->get('interests')->value;
      $interests[$interest_field] = $value;
      $tracking->set('interests', $interests);
      $tracking->save();
      
      return new JsonResponse(['status' => 'updated']);
    }
  }
  
  return new JsonResponse(['status' => 'error'], 400);
}

5. Form Submission Tracking

/**
 * Implements hook_webform_submission_form_alter().
 */
function webform_institution_tracking_webform_submission_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
  // Add the tracking ID to the form as a hidden field 
  $form['tracking_id'] = [
    '#type' => 'hidden',
    '#default_value' => '',
    '#attributes' => [
      'class' => ['tracking-id-field'],
    ],
  ];
  
  // Add JavaScript to populate the hidden field with the tracking ID
  $form['#attached']['library'][] = 'webform_institution_tracking/tracking_script';
  
  // Add a submit handler to update the tracking record
  $form['actions']['submit']['#submit'][] = 'webform_institution_tracking_submission_handler';
}

/**
 * Custom submit handler for webform.
 */
function webform_institution_tracking_submission_handler(&$form, \Drupal\Core\Form\FormStateInterface $form_state) {
  $tracking_id = $form_state->getValue('tracking_id');
  
  if (!empty($tracking_id)) {
    $tracking = \Drupal\webform_institution_tracking\Entity\InstitutionTracking::load($tracking_id);
    if ($tracking) {
      $tracking->set('form_submitted', TRUE);
      $tracking->save();
    }
  }
}

6. Report Download Tracking

/**
 * Implements hook_file_download().
 */
function webform_institution_tracking_file_download($uri) {
  // Check if this is a report download
  if (strpos($uri, 'webform-reports') !== FALSE) {
    $tracking_id = \Drupal::request()->query->get('tracking');
    
    if (!empty($tracking_id)) {
      $tracking = \Drupal\webform_institution_tracking\Entity\InstitutionTracking::load($tracking_id);
      if ($tracking) {
        $tracking->set('report_downloaded', TRUE);
        $tracking->save();
      }
    }
  }
}

7. Admin Interface with Views

Create a View to display:

  • Provider name
  • Date/time of visit
  • Which tools they were interested in (checkboxes)
  • Whether they submitted the form
  • Whether they downloaded the report

Add filters for:

  • Date range
  • Provider name
  • Interest areas
  • Completion status

8. JavaScript to Pass Tracking ID on Download

// Inject tracking ID into report download links
$('.report-download-link').once('tracking-injection').each(function() {
  var trackingId = sessionStorage.getItem('webform_tracking_id');
  if (trackingId) {
    var href = $(this).attr('href');
    if (href.indexOf('?') !== -1) {
      $(this).attr('href', href + '&tracking=' + trackingId);
    } else {
      $(this).attr('href', href + '?tracking=' + trackingId);
    }
  }
});

This solution:

  1. Creates a record when a provider is first selected
  2. Tracks which interest areas they select (the 3 checkboxes)
  3. Records form submission and report download as boolean flags
  4. Uses session and IP to identify unique/repeat visitors
  5. Provides admin reporting via Views
  6. Doesn't track detailed form data, only high-level interest and completion status

Would you like me to provide more detail on any specific part of this implementation?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment