diff --git a/make_post_sell/models/page_session.py b/make_post_sell/models/page_session.py index 3dab7f3..1dbc91b 100644 --- a/make_post_sell/models/page_session.py +++ b/make_post_sell/models/page_session.py @@ -1,6 +1,6 @@ import uuid -from sqlalchemy import Column, Integer, BigInteger, Boolean, Float, SmallInteger +from sqlalchemy import Column, Integer, BigInteger, Boolean, Float, SmallInteger, Unicode from .meta import Base, RBase, UUIDType, now_timestamp, foreign_key @@ -41,6 +41,8 @@ class PageSession(RBase, Base): is_ring_entry = Column(Boolean, nullable=True) ring_position = Column(SmallInteger, nullable=True) referrer_class = Column(SmallInteger, nullable=True) # 0-4 + referrer_domain = Column(Unicode(128), nullable=True) # e.g. "google.com" + referrer_query = Column(Unicode(256), nullable=True) # search engine query device_class = Column(SmallInteger, nullable=True) # 0-2 is_owner = Column(Boolean, nullable=True) # shop owner/editor viewing own product diff --git a/make_post_sell/scripts/alembic/versions/f3086e09b052_add_referrer_domain_and_referrer_query_.py b/make_post_sell/scripts/alembic/versions/f3086e09b052_add_referrer_domain_and_referrer_query_.py new file mode 100644 index 0000000..b63cb97 --- /dev/null +++ b/make_post_sell/scripts/alembic/versions/f3086e09b052_add_referrer_domain_and_referrer_query_.py @@ -0,0 +1,40 @@ +"""add referrer_domain and referrer_query to page_session + +Revision ID: f3086e09b052 +Revises: b3f7a2c8d1e5 +Create Date: 2026-02-26 16:53:40.340946 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f3086e09b052' +down_revision = 'b3f7a2c8d1e5' +branch_labels = None +depends_on = None + + +def _column_exists(table, column): + conn = op.get_bind() + result = conn.execute(sa.text(f"PRAGMA table_info({table})")) + return any(row[1] == column for row in result.fetchall()) + + +def upgrade(): + if not _column_exists("mps_page_session", "referrer_domain"): + op.add_column( + "mps_page_session", + sa.Column("referrer_domain", sa.Unicode(128), nullable=True), + ) + if not _column_exists("mps_page_session", "referrer_query"): + op.add_column( + "mps_page_session", + sa.Column("referrer_query", sa.Unicode(256), nullable=True), + ) + + +def downgrade(): + op.drop_column("mps_page_session", "referrer_query") + op.drop_column("mps_page_session", "referrer_domain") diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 45c492b..2d560a1 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -3499,6 +3499,16 @@ textarea { } } +.analytics-bar-row--wide .analytics-bar-label { + width: 140px; +} + +@media (min-width: 960px) { + .analytics-bar-row--wide .analytics-bar-label { + width: 180px; + } +} + .price-history-current td { font-weight: bold; } diff --git a/make_post_sell/templates/analytics.j2 b/make_post_sell/templates/analytics.j2 index 0da40ce..79c0e30 100644 --- a/make_post_sell/templates/analytics.j2 +++ b/make_post_sell/templates/analytics.j2 @@ -6,6 +6,37 @@ {% block content -%} +{% macro line_chart(data, max_value, label, color) %} +{% if data and max_value > 0 %} +
+ + {% for pct in [25, 50, 75] %} + + {% endfor %} + + + + {% for b in data %} + + {{ b.label }}: {{ b.tooltip }} + + {% endfor %} + {% for b in data %} + {% if loop.index0 % 7 == 0 %} + {{ b.label }} + {% endif %} + {% endfor %} + +
+{% endif %} +{% endmacro %} +

Analytics

@@ -68,6 +99,25 @@ {% endif %} + {# --- Trend Line Charts (28 days) --- #} + {% if daily_session_duration_max > 0 %} +

Session Duration Trend (last 28 days)

+

Average time visitors spend per session.

+ {{ line_chart(daily_session_duration, daily_session_duration_max, "Session duration trend", "var(--blue-color, #98b6fa)") }} + {% endif %} + + {% if daily_engagement %} +

Engagement Trend (last 28 days)

+

Active interaction time as a fraction of total session time.

+ {{ line_chart(daily_engagement, 1.0, "Engagement trend", "var(--green-color, #a3c765)") }} + {% endif %} + + {% if daily_bounce %} +

Bounce Rate Trend (last 28 days)

+

Sessions under 7 seconds visible. Lower is better.

+ {{ line_chart(daily_bounce, 1.0, "Bounce rate trend", "var(--red-color, #bc2131)") }} + {% endif %} + {# --- Ring Consumed breakdown --- #}

Ring Consumed

Unique products viewed via the ring as a percentage of all {{ ring_size }} ring products.

@@ -375,6 +425,73 @@ {% endif %} + {# --- Top Referrer Domains (21d) --- #} + {% if top_referrer_domains %} +

Top Referrer Domains (last 21 days)

+

External sites sending visitors to your shop.

+
+ {% for d in top_referrer_domains %} +
+ {{ d.domain }} + + + + {{ d.count }} ({{ d.pct }}) +
+ {% endfor %} +
+ {% endif %} + + {# --- Search Engine Queries (21d) --- #} + {% if top_referrer_queries %} +

Search Engine Queries (last 21 days)

+

What people searched before finding your shop.

+
+ + + + + + + + + {% for q in top_referrer_queries %} + + + + + {% endfor %} + +
QueryVisits
{{ q.query }}{{ q.count }}
+
+ {% endif %} + + {# --- Internal Search Keywords (21d) --- #} + {% if top_keywords %} +

Top Search Keywords (last 21 days)

+

What visitors search for inside your shop.

+
+ + + + + + + + + + {% for k in top_keywords %} + + + + + + {% endfor %} + +
KeywordsSearchesAvg Hits
{{ k.keywords }}{{ k.search_count }}{{ k.avg_hits }}
+
+ {% endif %} + {% endif %}{# end overview.total_views check #} {# --- Comment Sentiment (21d) --- #} diff --git a/make_post_sell/templates/analytics_product.j2 b/make_post_sell/templates/analytics_product.j2 index 30d15c6..7c8b645 100644 --- a/make_post_sell/templates/analytics_product.j2 +++ b/make_post_sell/templates/analytics_product.j2 @@ -6,6 +6,37 @@ {% block content -%} +{% macro line_chart(data, max_value, label, color) %} +{% if data and max_value > 0 %} +
+ + {% for pct in [25, 50, 75] %} + + {% endfor %} + + + + {% for b in data %} + + {{ b.label }}: {{ b.tooltip }} + + {% endfor %} + {% for b in data %} + {% if loop.index0 % 7 == 0 %} + {{ b.label }} + {% endif %} + {% endfor %} + +
+{% endif %} +{% endmacro %} +

← Shop Analytics · View Product

@@ -64,6 +95,25 @@ {% endif %} + {# --- Trend Line Charts (28 days) --- #} + {% if daily_session_duration_max > 0 %} +

Session Duration Trend (last 28 days)

+

Average time visitors spend per session.

+ {{ line_chart(daily_session_duration, daily_session_duration_max, "Session duration trend", "var(--blue-color, #98b6fa)") }} + {% endif %} + + {% if daily_engagement %} +

Engagement Trend (last 28 days)

+

Active interaction time as a fraction of total session time.

+ {{ line_chart(daily_engagement, 1.0, "Engagement trend", "var(--green-color, #a3c765)") }} + {% endif %} + + {% if daily_bounce %} +

Bounce Rate Trend (last 28 days)

+

Sessions under 7 seconds visible. Lower is better.

+ {{ line_chart(daily_bounce, 1.0, "Bounce rate trend", "var(--red-color, #bc2131)") }} + {% endif %} + {# --- Views Over Time --- #}

Views Over Time

@@ -168,6 +218,47 @@
{% endif %} + {# --- Top Referrer Domains (21d) --- #} + {% if top_referrer_domains %} +

Top Referrer Domains (last 21 days)

+

External sites sending visitors to this product.

+
+ {% for d in top_referrer_domains %} +
+ {{ d.domain }} + + + + {{ d.count }} ({{ d.pct }}) +
+ {% endfor %} +
+ {% endif %} + + {# --- Search Engine Queries (21d) --- #} + {% if top_referrer_queries %} +

Search Engine Queries (last 21 days)

+

What people searched before finding this product.

+
+ + + + + + + + + {% for q in top_referrer_queries %} + + + + + {% endfor %} + +
QueryVisits
{{ q.query }}{{ q.count }}
+
+ {% endif %} + {# --- Ring Entries --- #} {% if ring_entry_count > 0 %}

Ring Entry Sessions (last 21 days)

diff --git a/make_post_sell/tests/test_models.py b/make_post_sell/tests/test_models.py index 009993f..608d870 100644 --- a/make_post_sell/tests/test_models.py +++ b/make_post_sell/tests/test_models.py @@ -3188,43 +3188,76 @@ class TestProductViewCount(unittest.TestCase): class TestSignalClassifiers(unittest.TestCase): """Test classify_referrer and classify_device functions.""" - # --- classify_referrer --- + # --- classify_referrer (returns (class, domain, query) tuple) --- def test_referrer_direct_empty(self): - self.assertEqual(classify_referrer("", "example.com"), 0) + self.assertEqual(classify_referrer("", "example.com"), (0, None, None)) def test_referrer_direct_none(self): - self.assertEqual(classify_referrer(None, "example.com"), 0) + self.assertEqual(classify_referrer(None, "example.com"), (0, None, None)) def test_referrer_search_google(self): - self.assertEqual( - classify_referrer("https://www.google.com/search?q=foo", "example.com"), 1 + cls, domain, query = classify_referrer( + "https://www.google.com/search?q=foo", "example.com" ) + self.assertEqual(cls, 1) + self.assertEqual(domain, "www.google.com") + self.assertEqual(query, "foo") def test_referrer_search_duckduckgo(self): - self.assertEqual( - classify_referrer("https://duckduckgo.com/?q=bar", "example.com"), 1 + cls, domain, query = classify_referrer( + "https://duckduckgo.com/?q=bar", "example.com" ) + self.assertEqual(cls, 1) + self.assertEqual(domain, "duckduckgo.com") + self.assertEqual(query, "bar") + + def test_referrer_search_no_query(self): + cls, domain, query = classify_referrer( + "https://www.google.com/", "example.com" + ) + self.assertEqual(cls, 1) + self.assertEqual(domain, "www.google.com") + self.assertIsNone(query) def test_referrer_social_twitter(self): - self.assertEqual( - classify_referrer("https://twitter.com/user/status/123", "example.com"), 2 + cls, domain, query = classify_referrer( + "https://twitter.com/user/status/123", "example.com" ) + self.assertEqual(cls, 2) + self.assertEqual(domain, "twitter.com") + self.assertIsNone(query) def test_referrer_social_reddit(self): - self.assertEqual( - classify_referrer("https://www.reddit.com/r/test", "example.com"), 2 + cls, domain, query = classify_referrer( + "https://www.reddit.com/r/test", "example.com" ) + self.assertEqual(cls, 2) + self.assertEqual(domain, "www.reddit.com") + self.assertIsNone(query) def test_referrer_internal(self): - self.assertEqual( - classify_referrer("https://example.com/some/page", "example.com"), 3 + cls, domain, query = classify_referrer( + "https://example.com/some/page", "example.com" ) + self.assertEqual(cls, 3) + self.assertEqual(domain, "example.com") + self.assertIsNone(query) def test_referrer_unknown_external(self): - self.assertEqual( - classify_referrer("https://randomsite.org/page", "example.com"), 4 + cls, domain, query = classify_referrer( + "https://randomsite.org/page", "example.com" ) + self.assertEqual(cls, 4) + self.assertEqual(domain, "randomsite.org") + self.assertIsNone(query) + + def test_referrer_yahoo_query(self): + cls, domain, query = classify_referrer( + "https://search.yahoo.com/search?p=beats", "example.com" + ) + self.assertEqual(cls, 1) + self.assertEqual(query, "beats") # --- classify_device --- diff --git a/make_post_sell/views/analytics.py b/make_post_sell/views/analytics.py index 13fcfe9..d565c1a 100644 --- a/make_post_sell/views/analytics.py +++ b/make_post_sell/views/analytics.py @@ -110,6 +110,209 @@ def _daily_buckets(dbsession, base_filter, days=28): return buckets +def _daily_avg_duration(dbsession, base_filter, days=28): + """Bucket page sessions into daily avg wall_clock_ms for a line chart. + + Returns oldest-first list of {"day_offset", "label", "value", "tooltip"}. + """ + now = now_timestamp() + cutoff = now - days * DAY_MS + day_offset_expr = cast((now - PageSession.created_timestamp) / DAY_MS, Integer) + + rows = ( + dbsession.query( + day_offset_expr.label("day_offset"), + func.avg(PageSession.wall_clock_ms).label("avg_ms"), + ) + .filter(base_filter, PageSession.created_timestamp > cutoff) + .group_by(day_offset_expr) + .all() + ) + avgs = {r.day_offset: r.avg_ms or 0 for r in rows} + + today = datetime.now(timezone.utc).date() + buckets = [] + for i in range(days - 1, -1, -1): + d = today - timedelta(days=i) + val = avgs.get(i, 0) + buckets.append({ + "day_offset": i, + "label": d.strftime("%b %-d"), + "value": val, + "tooltip": _fmt_ms(val), + }) + return buckets + + +def _daily_engagement(dbsession, base_filter, days=28): + """Bucket sessions into daily engagement ratio (active_ms / wall_clock_ms). + + Returns oldest-first list with value as 0-1 float. + """ + now = now_timestamp() + cutoff = now - days * DAY_MS + day_offset_expr = cast((now - PageSession.created_timestamp) / DAY_MS, Integer) + + rows = ( + dbsession.query( + day_offset_expr.label("day_offset"), + func.avg( + cast(PageSession.active_ms, Float) / func.nullif(PageSession.wall_clock_ms, 0) + ).label("ratio"), + ) + .filter(base_filter, PageSession.created_timestamp > cutoff) + .group_by(day_offset_expr) + .all() + ) + ratios = {r.day_offset: r.ratio or 0 for r in rows} + + today = datetime.now(timezone.utc).date() + buckets = [] + for i in range(days - 1, -1, -1): + d = today - timedelta(days=i) + val = ratios.get(i, 0) + buckets.append({ + "day_offset": i, + "label": d.strftime("%b %-d"), + "value": val, + "tooltip": _fmt_pct(val), + }) + return buckets + + +def _daily_bounce_rate(dbsession, all_sessions_filter, days=28): + """Bucket sessions into daily bounce rate (visible < 7s / total). + + all_sessions_filter must NOT include the 7s visible floor. + Returns oldest-first list with value as 0-1 float. + """ + now = now_timestamp() + cutoff = now - days * DAY_MS + day_offset_expr = cast((now - PageSession.created_timestamp) / DAY_MS, Integer) + + rows = ( + dbsession.query( + day_offset_expr.label("day_offset"), + func.count().label("total"), + func.sum(case( + (PageSession.visible_ms < 7000, 1), else_=0 + )).label("bounced"), + ) + .filter(all_sessions_filter, PageSession.created_timestamp > cutoff) + .group_by(day_offset_expr) + .all() + ) + rates = {} + for r in rows: + if r.total and r.total > 0: + rates[r.day_offset] = (r.bounced or 0) / r.total + else: + rates[r.day_offset] = 0 + + today = datetime.now(timezone.utc).date() + buckets = [] + for i in range(days - 1, -1, -1): + d = today - timedelta(days=i) + val = rates.get(i, 0) + buckets.append({ + "day_offset": i, + "label": d.strftime("%b %-d"), + "value": val, + "tooltip": _fmt_pct(val), + }) + return buckets + + +def _top_search_keywords(dbsession, shop_id, days=21, limit=21): + """Top internal search keywords for a shop, last N days.""" + from ..models.shop_search_request import ShopSearchRequest + cutoff = now_timestamp() - days * DAY_MS + rows = ( + dbsession.query( + ShopSearchRequest.keywords, + func.count().label("search_count"), + func.sum(ShopSearchRequest.hit_count).label("total_hits"), + ) + .filter( + ShopSearchRequest.shop_id == shop_id, + ShopSearchRequest.created_timestamp > cutoff, + ) + .group_by(ShopSearchRequest.keywords) + .order_by(func.count().desc()) + .limit(limit) + .all() + ) + return [ + { + "keywords": r.keywords, + "search_count": r.search_count, + "total_hits": r.total_hits or 0, + "avg_hits": round((r.total_hits or 0) / max(r.search_count, 1), 1), + } + for r in rows + ] + + +def _top_referrer_domains(dbsession, base_filter, days=21, limit=21): + """Top referrer domains, excluding internal and direct.""" + cutoff = now_timestamp() - days * DAY_MS + rows = ( + dbsession.query( + PageSession.referrer_domain, + func.count().label("cnt"), + ) + .filter( + base_filter, + PageSession.created_timestamp > cutoff, + PageSession.referrer_domain.isnot(None), + PageSession.referrer_class != 3, # exclude internal + PageSession.referrer_class != 0, # exclude direct + ) + .group_by(PageSession.referrer_domain) + .order_by(func.count().desc()) + .limit(limit) + .all() + ) + total = sum(r.cnt for r in rows) or 1 + return [ + { + "domain": r.referrer_domain, + "count": r.cnt, + "pct": _fmt_pct(r.cnt / total), + "pct_raw": r.cnt / total * 100, + } + for r in rows + ] + + +def _top_referrer_queries(dbsession, base_filter, days=21, limit=21): + """Top search queries from external search engines.""" + cutoff = now_timestamp() - days * DAY_MS + rows = ( + dbsession.query( + PageSession.referrer_query, + func.count().label("cnt"), + ) + .filter( + base_filter, + PageSession.created_timestamp > cutoff, + PageSession.referrer_query.isnot(None), + PageSession.referrer_query != "", + ) + .group_by(PageSession.referrer_query) + .order_by(func.count().desc()) + .limit(limit) + .all() + ) + return [ + { + "query": r.referrer_query, + "count": r.cnt, + } + for r in rows + ] + + @view_config(route_name="shop_analytics", renderer="analytics.j2") @shop_editor_required() def shop_analytics(request): @@ -149,6 +352,15 @@ def shop_analytics(request): # --- Daily views (28-day bar chart) --- daily_views = _daily_buckets(db, _view_base, 28) + # --- Line chart trends (28 days) --- + daily_session_duration = _daily_avg_duration(db, _view_base, 28) + daily_engagement = _daily_engagement(db, _view_base, 28) + _all_sessions_base = and_( + PageSession.shop_id == shop.id, + _not_owner, + ) + daily_bounce = _daily_bounce_rate(db, _all_sessions_base, 28) + # --- Section 1: Overview Strip (last 7 days) --- ov = db.query( @@ -517,11 +729,22 @@ def shop_analytics(request): daily_views_max = max((b["count"] for b in daily_views), default=0) + # --- Internal Search Keywords (21d) --- + top_keywords = _top_search_keywords(db, shop.id, 21, 21) + + # --- Referrer Domains & Search Queries (21d) --- + top_referrer_domains = _top_referrer_domains(db, _view_base, 21, 21) + top_referrer_queries = _top_referrer_queries(db, _view_base, 21, 21) + return { "overview": overview, "ring_size": ring_size, "daily_views": daily_views, "daily_views_max": daily_views_max, + "daily_session_duration": daily_session_duration, + "daily_session_duration_max": max((b["value"] for b in daily_session_duration), default=0), + "daily_engagement": daily_engagement, + "daily_bounce": daily_bounce, "top_products": top_products, "ring_entries": ring_entries, "engagement": _ranked(eng_raw, _fmt_pct), @@ -532,6 +755,9 @@ def shop_analytics(request): "video_products": video_products, "sentiment": sentiment, "price_changes": price_changes, + "top_keywords": top_keywords, + "top_referrer_domains": top_referrer_domains, + "top_referrer_queries": top_referrer_queries, "traffic": [ { "source": REFERRER_LABELS.get(r.referrer_class, "Unknown"), @@ -596,6 +822,15 @@ def product_analytics(request): ) daily_views = _daily_buckets(db, _pf_base, 28) + # --- Line chart trends (28 days) --- + daily_session_duration = _daily_avg_duration(db, _pf_base, 28) + daily_engagement_data = _daily_engagement(db, _pf_base, 28) + _all_product_sessions = and_( + PageSession.product_id == product.id, + _not_owner, + ) + daily_bounce = _daily_bounce_rate(db, _all_product_sessions, 28) + # --- Overview --- ov = db.query( func.count().label("views"), @@ -766,6 +1001,10 @@ def product_analytics(request): daily_views_max = max((b["count"] for b in daily_views), default=0) + # --- Referrer Domains & Search Queries (21d) --- + top_referrer_domains = _top_referrer_domains(db, _pf_base, 21, 21) + top_referrer_queries = _top_referrer_queries(db, _pf_base, 21, 21) + return { "product": product, "product_url": product_url, @@ -773,6 +1012,10 @@ def product_analytics(request): "views_over_time": views_over_time, "daily_views": daily_views, "daily_views_max": daily_views_max, + "daily_session_duration": daily_session_duration, + "daily_session_duration_max": max((b["value"] for b in daily_session_duration), default=0), + "daily_engagement": daily_engagement_data, + "daily_bounce": daily_bounce, "video": video, "traffic": traffic, "devices": devices, @@ -780,4 +1023,6 @@ def product_analytics(request): "engagement": engagement, "sentiment": sentiment, "price_changes": price_changes, + "top_referrer_domains": top_referrer_domains, + "top_referrer_queries": top_referrer_queries, } diff --git a/make_post_sell/views/signals.py b/make_post_sell/views/signals.py index 05cad95..27661e5 100644 --- a/make_post_sell/views/signals.py +++ b/make_post_sell/views/signals.py @@ -29,42 +29,59 @@ _SOCIAL_DOMAINS = { } -def classify_referrer(referrer, request_host): - """Classify a Referer header into 0-4. +# Search engine query parameter names by domain root +_SEARCH_QUERY_PARAMS = { + "google": "q", "bing": "q", "yahoo": "p", "duckduckgo": "q", + "baidu": "wd", "yandex": "text", "ecosia": "q", "qwant": "q", + "startpage": "query", "brave": "q", +} - 0 = direct (no referrer) - 1 = search engine - 2 = social media - 3 = internal (same host) - 4 = unknown (other external) + +def classify_referrer(referrer, request_host): + """Classify a Referer header into (class, domain, query). + + class: 0=direct, 1=search, 2=social, 3=internal, 4=unknown + domain: hostname string or None + query: search engine query string or None """ if not referrer: - return 0 + return 0, None, None try: - # Extract domain from referrer URL - # e.g. "https://www.google.com/search?q=foo" -> "google" - from urllib.parse import urlparse + from urllib.parse import urlparse, parse_qs parsed = urlparse(referrer) host = (parsed.hostname or "").lower() except Exception: - return 4 + return 4, None, None + + domain = host or None # Internal: same host as request if request_host and host == request_host.lower(): - return 3 + return 3, domain, None - # Strip www. and extract root domain - if host.startswith("www."): - host = host[4:] - # Get the second-level domain: "search.google.com" -> "google" - parts = host.split(".") - root = parts[-2] if len(parts) >= 2 else host + # Strip www. and extract root domain for classification + clean = host + if clean.startswith("www."): + clean = clean[4:] + parts = clean.split(".") + root = parts[-2] if len(parts) >= 2 else clean if root in _SEARCH_DOMAINS: - return 1 + query = None + param = _SEARCH_QUERY_PARAMS.get(root, "q") + try: + qs = parse_qs(parsed.query) + vals = qs.get(param, []) + if vals: + query = vals[0][:256] + except Exception: + pass + return 1, domain, query + if root in _SOCIAL_DOMAINS: - return 2 - return 4 + return 2, domain, None + + return 4, domain, None def classify_device(viewport_width): @@ -215,7 +232,10 @@ def beacon_view(request): request_host = request.host if ":" in request_host: request_host = request_host.split(":")[0] - ps.referrer_class = classify_referrer(referrer, request_host) + ref_class, ref_domain, ref_query = classify_referrer(referrer, request_host) + ps.referrer_class = ref_class + ps.referrer_domain = ref_domain[:128] if ref_domain else None + ps.referrer_query = ref_query[:256] if ref_query else None ps.device_class = classify_device(body.get("viewport_width")) ps.is_ring_entry = bool(body.get("is_ring_entry")) if body.get("is_ring_entry") is not None else None ps.ring_position = _clamp_int(body.get("ring_position"), -32768, 32767)