feat: add search result click tracking and fix content engagement labels
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

- Pass search_query_id to search results template
- Add POST /api/analytics/search-click endpoint to update SearchQuery
  with clicked_result_position, clicked_company_id, time_to_click_ms
- Add data-position and data-company-id attributes to company cards
- Add JS using navigator.sendBeacon for non-blocking click tracking
- Fix content engagement labels: "nowych (30 dni)" instead of "opublikowanych"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-10 20:23:49 +01:00
parent ad4acc7c62
commit 618bd9b8d3
4 changed files with 63 additions and 5 deletions

View File

@ -12,7 +12,7 @@ from datetime import datetime
from flask import jsonify, request, session, current_app
from flask_login import current_user
from database import SessionLocal, UserSession, UserClick, PageView, JSError
from database import SessionLocal, UserSession, UserClick, PageView, JSError, SearchQuery
from extensions import limiter
from . import bp
@ -250,3 +250,32 @@ def api_analytics_conversion():
)
return jsonify({'success': True}), 200
@bp.route('/analytics/search-click', methods=['POST'])
@limiter.limit("30/minute")
def api_analytics_search_click():
"""Track when a user clicks a search result."""
data = request.get_json()
if not data:
return jsonify({'error': 'No data'}), 400
search_query_id = data.get('search_query_id')
if not search_query_id:
return jsonify({'error': 'Missing search_query_id'}), 400
db = SessionLocal()
try:
sq = db.query(SearchQuery).filter_by(id=search_query_id).first()
if sq:
sq.clicked_result_position = data.get('position')
sq.clicked_company_id = data.get('company_id')
sq.time_to_click_ms = data.get('time_to_click_ms')
db.commit()
return jsonify({'success': True}), 200
except Exception as e:
logger.error(f"Search click tracking error: {e}")
db.rollback()
return jsonify({'success': False}), 200
finally:
db.close()

View File

@ -472,6 +472,7 @@ def search():
companies = [r.company for r in results]
# Log search to analytics (SearchQuery table)
search_query_id = None
if query:
try:
analytics_session_id = session.get('analytics_session_id')
@ -493,9 +494,11 @@ def search():
)
db.add(search_query)
db.commit()
search_query_id = search_query.id
except Exception as e:
logger.error(f"Search logging error: {e}")
db.rollback()
search_query_id = None
# For debugging/analytics - log search stats
if query:
@ -531,7 +534,8 @@ def search():
people=people_results,
query=query,
category_id=category_id,
result_count=len(companies)
result_count=len(companies),
search_query_id=search_query_id
)
finally:
db.close()

View File

@ -701,11 +701,11 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-sm); margin-bottom: var(--spacing-md);">
<div>
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ stats.published }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">opublikowanych</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">nowych (30 dni)</div>
</div>
<div>
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ stats.read_by }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">czytelników</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">aktywnych czytelników</div>
</div>
</div>
<div style="margin-bottom: 4px; display: flex; justify-content: space-between;">

View File

@ -286,7 +286,7 @@
{% if companies %}
<div class="companies-grid">
{% for company in companies %}
<div class="company-card">
<div class="company-card" data-position="{{ loop.index }}" data-company-id="{{ company.id }}">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="company-logo">
<img src="{{ url_for('static', filename='img/companies/' ~ company.slug ~ '.webp') }}"
alt="{{ company.name }}"
@ -364,3 +364,28 @@
</div>
{% endif %}
{% endblock %}
{% block extra_js %}
{% if search_query_id %}
(function() {
var sqId = {{ search_query_id }};
var t0 = Date.now();
document.querySelectorAll('.company-card').forEach(function(card) {
card.querySelectorAll('a').forEach(function(link) {
link.addEventListener('click', function() {
var pos = parseInt(card.dataset.position);
var cid = parseInt(card.dataset.companyId);
navigator.sendBeacon('/api/analytics/search-click',
new Blob([JSON.stringify({
search_query_id: sqId,
position: pos,
company_id: cid,
time_to_click_ms: Date.now() - t0
})], {type: 'application/json'})
);
});
});
});
})();
{% endif %}
{% endblock %}