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
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:
parent
ad4acc7c62
commit
618bd9b8d3
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;">
|
||||
|
||||
@ -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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user